cyclecad 3.8.0 → 3.9.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/app/index.html +96 -0
- package/app/js/modules/auto-assembly.js +1146 -0
- package/app/js/modules/digital-twin.js +1225 -0
- package/app/js/modules/engineering-notebook.js +1505 -0
- package/app/js/modules/machine-control.js +1270 -0
- package/app/js/modules/parametric-from-example.js +900 -0
- package/app/js/modules/smart-assembly.js +1667 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Built-in CNC / 3D Printer Control module for cycleCAD
|
|
3
|
+
* Direct browser-based machine control: FDM printers, SLA, CNC mills, laser cutters
|
|
4
|
+
* Supports WebSocket (Klipper/Duet/Smoothieware), OctoPrint API, Web Serial, Moonraker, demo mode
|
|
5
|
+
* G-code generation, toolpath preview, live jog controls, temperature monitoring
|
|
6
|
+
*
|
|
7
|
+
* Exports: window.CycleCAD.MachineControl
|
|
8
|
+
* Methods: init, getUI, execute, connect, sendGCode, getStatus, generateToolpath
|
|
9
|
+
*
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
14
|
+
|
|
15
|
+
window.CycleCAD.MachineControl = (() => {
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// STATE
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
let state = {
|
|
21
|
+
connected: false,
|
|
22
|
+
activeProfile: null,
|
|
23
|
+
machines: [],
|
|
24
|
+
currentMachine: null,
|
|
25
|
+
gCodeBuffer: [],
|
|
26
|
+
jobHistory: [],
|
|
27
|
+
isRunning: false,
|
|
28
|
+
isPaused: false,
|
|
29
|
+
currentLayer: 0,
|
|
30
|
+
totalLayers: 1,
|
|
31
|
+
startTime: null,
|
|
32
|
+
temperatures: { nozzle: 0, bed: 0, nozzleTarget: 0, bedTarget: 0 },
|
|
33
|
+
tempHistory: [],
|
|
34
|
+
position: { x: 0, y: 0, z: 0 },
|
|
35
|
+
feedOverride: 100,
|
|
36
|
+
spindleOverride: 100,
|
|
37
|
+
materialUsed: { length: 0, weight: 0 },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Machine profiles database
|
|
41
|
+
const MACHINE_PROFILES = {
|
|
42
|
+
ender3: {
|
|
43
|
+
name: 'Ender 3 / V2 / V3',
|
|
44
|
+
type: 'fdm',
|
|
45
|
+
bed: { x: 220, y: 220, z: 250 },
|
|
46
|
+
nozzle: 0.4,
|
|
47
|
+
gcodeFlavor: 'marlin',
|
|
48
|
+
tempNozzle: 200,
|
|
49
|
+
tempBed: 60,
|
|
50
|
+
speed: 150,
|
|
51
|
+
},
|
|
52
|
+
prusa: {
|
|
53
|
+
name: 'Prusa i3 MK3S+',
|
|
54
|
+
type: 'fdm',
|
|
55
|
+
bed: { x: 250, y: 210, z: 210 },
|
|
56
|
+
nozzle: 0.4,
|
|
57
|
+
gcodeFlavor: 'prusa',
|
|
58
|
+
tempNozzle: 215,
|
|
59
|
+
tempBed: 60,
|
|
60
|
+
speed: 200,
|
|
61
|
+
},
|
|
62
|
+
bambu: {
|
|
63
|
+
name: 'Bambu Lab X1C',
|
|
64
|
+
type: 'fdm',
|
|
65
|
+
bed: { x: 256, y: 256, z: 256 },
|
|
66
|
+
nozzle: 0.4,
|
|
67
|
+
gcodeFlavor: 'klipper',
|
|
68
|
+
tempNozzle: 220,
|
|
69
|
+
tempBed: 45,
|
|
70
|
+
speed: 300,
|
|
71
|
+
},
|
|
72
|
+
voron: {
|
|
73
|
+
name: 'Voron 2.4',
|
|
74
|
+
type: 'fdm',
|
|
75
|
+
bed: { x: 350, y: 350, z: 330 },
|
|
76
|
+
nozzle: 0.4,
|
|
77
|
+
gcodeFlavor: 'klipper',
|
|
78
|
+
tempNozzle: 220,
|
|
79
|
+
tempBed: 100,
|
|
80
|
+
speed: 250,
|
|
81
|
+
},
|
|
82
|
+
snapmaker: {
|
|
83
|
+
name: 'Snapmaker 2.0',
|
|
84
|
+
type: 'multi',
|
|
85
|
+
bed: { x: 200, y: 200, z: 200 },
|
|
86
|
+
nozzle: 0.4,
|
|
87
|
+
gcodeFlavor: 'marlin',
|
|
88
|
+
tempNozzle: 200,
|
|
89
|
+
tempBed: 0,
|
|
90
|
+
speed: 100,
|
|
91
|
+
},
|
|
92
|
+
cnc3018: {
|
|
93
|
+
name: 'Generic CNC 3018',
|
|
94
|
+
type: 'cnc',
|
|
95
|
+
bed: { x: 300, y: 180, z: 45 },
|
|
96
|
+
nozzle: 3.175,
|
|
97
|
+
gcodeFlavor: 'grbl',
|
|
98
|
+
tempNozzle: 0,
|
|
99
|
+
tempBed: 0,
|
|
100
|
+
speed: 100,
|
|
101
|
+
},
|
|
102
|
+
shapeoko: {
|
|
103
|
+
name: 'Shapeoko 4',
|
|
104
|
+
type: 'cnc',
|
|
105
|
+
bed: { x: 838, y: 838, z: 100 },
|
|
106
|
+
nozzle: 6.35,
|
|
107
|
+
gcodeFlavor: 'mach3',
|
|
108
|
+
tempNozzle: 0,
|
|
109
|
+
tempBed: 0,
|
|
110
|
+
speed: 150,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// 1. MACHINE CONNECTION
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Connect to a machine via specified protocol
|
|
120
|
+
* @param {Object} config - Connection config
|
|
121
|
+
* @param {string} config.protocol - 'websocket' | 'octoprint' | 'serial' | 'moonraker' | 'demo'
|
|
122
|
+
* @param {string} config.host - Host address (e.g., '192.168.1.100')
|
|
123
|
+
* @param {number} config.port - Port number
|
|
124
|
+
* @param {string} config.apiKey - API key (for OctoPrint, Moonraker)
|
|
125
|
+
* @param {string} config.profileKey - Machine profile key
|
|
126
|
+
* @returns {Promise<boolean>} True if connected successfully
|
|
127
|
+
*/
|
|
128
|
+
async function connect(config) {
|
|
129
|
+
try {
|
|
130
|
+
const profile = MACHINE_PROFILES[config.profileKey];
|
|
131
|
+
if (!profile) throw new Error('Unknown machine profile');
|
|
132
|
+
|
|
133
|
+
state.currentMachine = {
|
|
134
|
+
id: `machine_${Date.now()}`,
|
|
135
|
+
name: profile.name,
|
|
136
|
+
profile: profile,
|
|
137
|
+
config: config,
|
|
138
|
+
protocol: config.protocol,
|
|
139
|
+
connection: null,
|
|
140
|
+
lastResponse: null,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
switch (config.protocol) {
|
|
144
|
+
case 'websocket':
|
|
145
|
+
await connectWebSocket(config);
|
|
146
|
+
break;
|
|
147
|
+
case 'octoprint':
|
|
148
|
+
await connectOctoPrint(config);
|
|
149
|
+
break;
|
|
150
|
+
case 'serial':
|
|
151
|
+
await connectSerial(config);
|
|
152
|
+
break;
|
|
153
|
+
case 'moonraker':
|
|
154
|
+
await connectMoonraker(config);
|
|
155
|
+
break;
|
|
156
|
+
case 'demo':
|
|
157
|
+
connectDemo();
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
throw new Error('Unknown protocol: ' + config.protocol);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
state.connected = true;
|
|
164
|
+
state.machines.push(state.currentMachine);
|
|
165
|
+
console.log('[MachineControl] Connected to ' + profile.name);
|
|
166
|
+
return true;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('[MachineControl] Connection failed:', err);
|
|
169
|
+
state.connected = false;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Connect via WebSocket to Klipper/Duet/Smoothieware
|
|
176
|
+
*/
|
|
177
|
+
async function connectWebSocket(config) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const url = `ws://${config.host}:${config.port}`;
|
|
180
|
+
const ws = new WebSocket(url);
|
|
181
|
+
|
|
182
|
+
ws.onopen = () => {
|
|
183
|
+
state.currentMachine.connection = ws;
|
|
184
|
+
resolve();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
ws.onerror = (err) => reject(new Error('WebSocket error: ' + err.message));
|
|
188
|
+
ws.onclose = () => { state.connected = false; };
|
|
189
|
+
|
|
190
|
+
ws.onmessage = (evt) => {
|
|
191
|
+
state.currentMachine.lastResponse = evt.data;
|
|
192
|
+
parseResponse(evt.data);
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Connect via OctoPrint REST API + WebSocket
|
|
199
|
+
*/
|
|
200
|
+
async function connectOctoPrint(config) {
|
|
201
|
+
const baseURL = `http://${config.host}:${config.port}`;
|
|
202
|
+
const headers = { 'X-API-Key': config.apiKey };
|
|
203
|
+
|
|
204
|
+
// Test connection with simple API call
|
|
205
|
+
const resp = await fetch(`${baseURL}/api/version`, { headers });
|
|
206
|
+
if (!resp.ok) throw new Error('OctoPrint API unreachable');
|
|
207
|
+
|
|
208
|
+
// Establish WebSocket for real-time updates
|
|
209
|
+
const wsURL = `ws://${config.host}:${config.port}/sockjs/websocket`;
|
|
210
|
+
const ws = new WebSocket(wsURL);
|
|
211
|
+
|
|
212
|
+
ws.onopen = () => {
|
|
213
|
+
state.currentMachine.connection = { ws, baseURL, headers };
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
ws.onmessage = (evt) => {
|
|
217
|
+
const msg = JSON.parse(evt.data);
|
|
218
|
+
if (msg.event === 'Status') updateOctoPrintStatus(msg);
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Connect via Web Serial API (Chrome/Edge, direct USB)
|
|
224
|
+
*/
|
|
225
|
+
async function connectSerial(config) {
|
|
226
|
+
if (!navigator.serial) throw new Error('Web Serial API not supported');
|
|
227
|
+
|
|
228
|
+
const port = await navigator.serial.requestPort();
|
|
229
|
+
await port.open({ baudRate: config.baudRate || 115200 });
|
|
230
|
+
|
|
231
|
+
const writer = port.writable.getWriter();
|
|
232
|
+
const reader = port.readable.getReader();
|
|
233
|
+
|
|
234
|
+
state.currentMachine.connection = { port, writer, reader };
|
|
235
|
+
|
|
236
|
+
// Read responses from serial port
|
|
237
|
+
(async () => {
|
|
238
|
+
const decoder = new TextDecoderStream();
|
|
239
|
+
reader.pipeTo(decoder.writable);
|
|
240
|
+
const input = decoder.readable.getReader();
|
|
241
|
+
try {
|
|
242
|
+
while (true) {
|
|
243
|
+
const { value, done } = await input.read();
|
|
244
|
+
if (done) break;
|
|
245
|
+
state.currentMachine.lastResponse = value;
|
|
246
|
+
parseResponse(value);
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error('Serial read error:', err);
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Connect via Moonraker API (Klipper companion)
|
|
256
|
+
*/
|
|
257
|
+
async function connectMoonraker(config) {
|
|
258
|
+
const baseURL = `http://${config.host}:${config.port}`;
|
|
259
|
+
const wsURL = `ws://${config.host}:${config.port}/websocket`;
|
|
260
|
+
|
|
261
|
+
const ws = new WebSocket(wsURL);
|
|
262
|
+
ws.onopen = () => {
|
|
263
|
+
state.currentMachine.connection = { ws, baseURL };
|
|
264
|
+
// Request printer object updates
|
|
265
|
+
ws.send(JSON.stringify({
|
|
266
|
+
jsonrpc: '2.0',
|
|
267
|
+
method: 'printer.objects.subscribe',
|
|
268
|
+
params: { objects: { 'gcode_move': null, 'extruder': null, 'heater_bed': null } },
|
|
269
|
+
id: 1,
|
|
270
|
+
}));
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
ws.onmessage = (evt) => {
|
|
274
|
+
const msg = JSON.parse(evt.data);
|
|
275
|
+
if (msg.method === 'notify_update') updateMoonrakerStatus(msg.params);
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Demo/simulated machine connection
|
|
281
|
+
*/
|
|
282
|
+
function connectDemo() {
|
|
283
|
+
state.currentMachine.connection = {
|
|
284
|
+
type: 'demo',
|
|
285
|
+
buffer: [],
|
|
286
|
+
time: 0,
|
|
287
|
+
interval: null,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Simulate heating
|
|
291
|
+
simulateMachineResponse('M104 S' + state.currentMachine.profile.tempNozzle);
|
|
292
|
+
simulateMachineResponse('M140 S' + state.currentMachine.profile.tempBed);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse machine response and update state
|
|
297
|
+
*/
|
|
298
|
+
function parseResponse(data) {
|
|
299
|
+
// Handle M114 position report: X:10.50 Y:20.30 Z:5.00
|
|
300
|
+
const posMatch = data.match(/X:([\d.-]+)\s+Y:([\d.-]+)\s+Z:([\d.-]+)/i);
|
|
301
|
+
if (posMatch) {
|
|
302
|
+
state.position = { x: parseFloat(posMatch[1]), y: parseFloat(posMatch[2]), z: parseFloat(posMatch[3]) };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Handle temperature reports
|
|
306
|
+
const tempMatch = data.match(/T:([\d.]+)\/([\d.]+)\s+B:([\d.]+)\/([\d.]+)/);
|
|
307
|
+
if (tempMatch) {
|
|
308
|
+
state.temperatures.nozzle = parseFloat(tempMatch[1]);
|
|
309
|
+
state.temperatures.nozzleTarget = parseFloat(tempMatch[2]);
|
|
310
|
+
state.temperatures.bed = parseFloat(tempMatch[3]);
|
|
311
|
+
state.temperatures.bedTarget = parseFloat(tempMatch[4]);
|
|
312
|
+
recordTempHistory();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// 2. G-CODE GENERATOR (~350 lines)
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Generate G-code from 3D model geometry
|
|
322
|
+
* @param {Object} geometry - THREE.BufferGeometry
|
|
323
|
+
* @param {Object} settings - Generation settings
|
|
324
|
+
* @param {string} settings.mode - 'fdm' | 'cnc_mill' | 'laser'
|
|
325
|
+
* @param {number} settings.layerHeight - Layer height (mm)
|
|
326
|
+
* @param {number} settings.infillPercent - Infill %
|
|
327
|
+
* @param {number} settings.wallCount - Wall count
|
|
328
|
+
* @param {number} settings.supportType - 'tree' | 'linear' | 'none'
|
|
329
|
+
* @returns {string} G-code string
|
|
330
|
+
*/
|
|
331
|
+
function generateToolpath(geometry, settings) {
|
|
332
|
+
let gcode = generateHeader();
|
|
333
|
+
|
|
334
|
+
if (settings.mode === 'fdm') {
|
|
335
|
+
gcode += generateFDMGCode(geometry, settings);
|
|
336
|
+
} else if (settings.mode === 'cnc_mill') {
|
|
337
|
+
gcode += generateCNCGCode(geometry, settings);
|
|
338
|
+
} else if (settings.mode === 'laser') {
|
|
339
|
+
gcode += generateLaserGCode(geometry, settings);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
gcode += generateFooter();
|
|
343
|
+
state.gCodeBuffer = gcode.split('\n').filter(l => l.length > 0);
|
|
344
|
+
return gcode;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generate FDM G-code (simplified slicing)
|
|
349
|
+
*/
|
|
350
|
+
function generateFDMGCode(geometry, settings) {
|
|
351
|
+
let gcode = '';
|
|
352
|
+
const layerHeight = settings.layerHeight || 0.2;
|
|
353
|
+
const infill = settings.infillPercent || 20;
|
|
354
|
+
const walls = settings.wallCount || 2;
|
|
355
|
+
const tempNozzle = state.currentMachine.profile.tempNozzle;
|
|
356
|
+
const tempBed = state.currentMachine.profile.tempBed;
|
|
357
|
+
|
|
358
|
+
// Calculate bounding box
|
|
359
|
+
geometry.computeBoundingBox();
|
|
360
|
+
const bbox = geometry.boundingBox;
|
|
361
|
+
const height = bbox.max.z - bbox.min.z;
|
|
362
|
+
const numLayers = Math.ceil(height / layerHeight);
|
|
363
|
+
|
|
364
|
+
gcode += `;Generated by cycleCAD MachineControl\n`;
|
|
365
|
+
gcode += `;Layer height: ${layerHeight}\n`;
|
|
366
|
+
gcode += `;Infill: ${infill}%\n\n`;
|
|
367
|
+
|
|
368
|
+
// Preheat
|
|
369
|
+
gcode += `M104 S${tempNozzle}\n`;
|
|
370
|
+
gcode += `M140 S${tempBed}\n`;
|
|
371
|
+
gcode += `M109 S${tempNozzle}\n`;
|
|
372
|
+
gcode += `M190 S${tempBed}\n\n`;
|
|
373
|
+
|
|
374
|
+
// Homing & reset
|
|
375
|
+
gcode += `G28\n`;
|
|
376
|
+
gcode += `G92 E0\n`;
|
|
377
|
+
|
|
378
|
+
// Generate skirt (one loop around print area)
|
|
379
|
+
gcode += generateSkirt(geometry, settings) + '\n';
|
|
380
|
+
|
|
381
|
+
// Generate layers
|
|
382
|
+
for (let layer = 0; layer < numLayers; layer++) {
|
|
383
|
+
const z = bbox.min.z + layer * layerHeight;
|
|
384
|
+
gcode += `\n;Layer ${layer}/${numLayers}\n`;
|
|
385
|
+
gcode += `G0 Z${z.toFixed(3)}\n`;
|
|
386
|
+
|
|
387
|
+
// Walls (perimeters)
|
|
388
|
+
for (let wall = 0; wall < walls; wall++) {
|
|
389
|
+
gcode += generateWalls(geometry, z, wall) + '\n';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Infill
|
|
393
|
+
if (infill > 0) {
|
|
394
|
+
gcode += generateInfill(geometry, z, infill) + '\n';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Retractions & moves between features
|
|
399
|
+
gcode = applyRetraction(gcode, settings);
|
|
400
|
+
|
|
401
|
+
state.totalLayers = numLayers;
|
|
402
|
+
return gcode;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Generate CNC G-code (milling operations)
|
|
407
|
+
*/
|
|
408
|
+
function generateCNCGCode(geometry, settings) {
|
|
409
|
+
let gcode = '';
|
|
410
|
+
const toolDia = state.currentMachine.profile.nozzle;
|
|
411
|
+
const depth = settings.depth || 10;
|
|
412
|
+
const stepDown = settings.stepDown || 5;
|
|
413
|
+
const speed = state.currentMachine.profile.speed;
|
|
414
|
+
const feedRate = settings.feedRate || 600;
|
|
415
|
+
|
|
416
|
+
gcode += `;CNC Milling Program\n`;
|
|
417
|
+
gcode += `;Tool diameter: ${toolDia}mm\n`;
|
|
418
|
+
gcode += `;Feed rate: ${feedRate}mm/min\n\n`;
|
|
419
|
+
|
|
420
|
+
// Home
|
|
421
|
+
gcode += `G28\n`;
|
|
422
|
+
gcode += `G92 X0 Y0 Z0\n`;
|
|
423
|
+
|
|
424
|
+
// Setup
|
|
425
|
+
gcode += `G21\n`; // Metric
|
|
426
|
+
gcode += `G17\n`; // XY plane
|
|
427
|
+
gcode += `G40\n`; // Cancel cutter radius comp
|
|
428
|
+
gcode += `G90\n`; // Absolute positioning
|
|
429
|
+
gcode += `M3 S${speed}\n`; // Spindle on
|
|
430
|
+
gcode += `G4 P2\n`; // Wait 2s for spindle
|
|
431
|
+
gcode += `G0 Z${5}\n\n`; // Retract
|
|
432
|
+
|
|
433
|
+
// Generate pocketing operation (simplified)
|
|
434
|
+
gcode += generatePocket(geometry, depth, stepDown) + '\n';
|
|
435
|
+
|
|
436
|
+
// End
|
|
437
|
+
gcode += `G0 Z${10}\n`;
|
|
438
|
+
gcode += `M5\n`; // Spindle off
|
|
439
|
+
gcode += `G28\n`; // Home
|
|
440
|
+
|
|
441
|
+
return gcode;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Generate laser cutting G-code
|
|
446
|
+
*/
|
|
447
|
+
function generateLaserGCode(geometry, settings) {
|
|
448
|
+
let gcode = '';
|
|
449
|
+
const power = settings.power || 80;
|
|
450
|
+
const cutSpeed = settings.cutSpeed || 100;
|
|
451
|
+
const engraveSpeed = settings.engraveSpeed || 300;
|
|
452
|
+
|
|
453
|
+
gcode += `;Laser Cutting Program\n`;
|
|
454
|
+
gcode += `;Power: ${power}%\n\n`;
|
|
455
|
+
|
|
456
|
+
gcode += `G28\n`;
|
|
457
|
+
gcode += `G92 X0 Y0\n`;
|
|
458
|
+
gcode += `M3\n`; // Laser on (M4 for variable power)
|
|
459
|
+
gcode += `M107\n`; // Aux off
|
|
460
|
+
|
|
461
|
+
// Generate outline cuts
|
|
462
|
+
gcode += generateLaserPath(geometry, cutSpeed, power) + '\n';
|
|
463
|
+
|
|
464
|
+
gcode += `M5\n`; // Laser off
|
|
465
|
+
|
|
466
|
+
return gcode;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Generate skirt (adhesion perimeter)
|
|
471
|
+
*/
|
|
472
|
+
function generateSkirt(geometry, settings) {
|
|
473
|
+
const offset = settings.skirtOffset || 5;
|
|
474
|
+
let gcode = `;Skirt\n`;
|
|
475
|
+
gcode += `G0 F${state.currentMachine.profile.speed * 60}\n`;
|
|
476
|
+
gcode += `G0 X${-offset} Y${-offset}\n`;
|
|
477
|
+
gcode += `G0 Z0.2\n`;
|
|
478
|
+
gcode += `G1 E2\n`; // Prime
|
|
479
|
+
gcode += `G1 X${offset} Y${-offset} E5\n`;
|
|
480
|
+
gcode += `G1 X${offset} Y${offset} E8\n`;
|
|
481
|
+
gcode += `G1 X${-offset} Y${offset} E11\n`;
|
|
482
|
+
gcode += `G1 X${-offset} Y${-offset} E14\n`;
|
|
483
|
+
return gcode;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Generate wall/perimeter paths
|
|
488
|
+
*/
|
|
489
|
+
function generateWalls(geometry, z, wallIndex) {
|
|
490
|
+
const offset = wallIndex * 2;
|
|
491
|
+
let gcode = `;Wall ${wallIndex}\n`;
|
|
492
|
+
gcode += `G1 F${state.currentMachine.profile.speed * 60}\n`;
|
|
493
|
+
|
|
494
|
+
// Simplified: square spiral
|
|
495
|
+
const size = 50;
|
|
496
|
+
const x = offset, y = offset;
|
|
497
|
+
gcode += `G1 X${x} Y${y}\n`;
|
|
498
|
+
gcode += `G1 X${size - offset} Y${y} E10\n`;
|
|
499
|
+
gcode += `G1 X${size - offset} Y${size - offset} E20\n`;
|
|
500
|
+
gcode += `G1 X${x} Y${size - offset} E30\n`;
|
|
501
|
+
gcode += `G1 X${x} Y${y} E40\n`;
|
|
502
|
+
|
|
503
|
+
return gcode;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Generate infill pattern
|
|
508
|
+
*/
|
|
509
|
+
function generateInfill(geometry, z, infillPercent) {
|
|
510
|
+
let gcode = `;Infill ${infillPercent}%\n`;
|
|
511
|
+
const lineSpacing = 2 / (infillPercent / 100);
|
|
512
|
+
gcode += `G1 F${state.currentMachine.profile.speed * 60}\n`;
|
|
513
|
+
|
|
514
|
+
// Simplified: horizontal lines
|
|
515
|
+
for (let y = 0; y < 50; y += lineSpacing) {
|
|
516
|
+
if (y % (lineSpacing * 2) < lineSpacing) {
|
|
517
|
+
gcode += `G1 X0 Y${y} E${y}\n`;
|
|
518
|
+
gcode += `G1 X50 Y${y} E${y + 50}\n`;
|
|
519
|
+
} else {
|
|
520
|
+
gcode += `G1 X50 Y${y} E${y}\n`;
|
|
521
|
+
gcode += `G1 X0 Y${y} E${y + 50}\n`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return gcode;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Generate CNC pocketing operation
|
|
530
|
+
*/
|
|
531
|
+
function generatePocket(geometry, depth, stepDown) {
|
|
532
|
+
let gcode = '';
|
|
533
|
+
const numPasses = Math.ceil(depth / stepDown);
|
|
534
|
+
|
|
535
|
+
for (let pass = 0; pass < numPasses; pass++) {
|
|
536
|
+
const z = -(pass + 1) * stepDown;
|
|
537
|
+
gcode += `\n;Pass ${pass + 1}/${numPasses} (Z=${z})\n`;
|
|
538
|
+
gcode += `G0 Z5\n`;
|
|
539
|
+
gcode += `G0 X10 Y10\n`;
|
|
540
|
+
gcode += `G1 Z${z} F200\n`; // Plunge
|
|
541
|
+
gcode += `G1 X40 Y10 F600\n`; // Move
|
|
542
|
+
gcode += `G1 X40 Y40\n`;
|
|
543
|
+
gcode += `G1 X10 Y40\n`;
|
|
544
|
+
gcode += `G1 X10 Y10\n`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return gcode;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Generate laser cutting path
|
|
552
|
+
*/
|
|
553
|
+
function generateLaserPath(geometry, speed, power) {
|
|
554
|
+
let gcode = '';
|
|
555
|
+
gcode += `G1 F${speed}\n`;
|
|
556
|
+
gcode += `S${Math.round(power * 2.55)}\n`; // Convert % to 0-255
|
|
557
|
+
gcode += `G1 X10 Y10\n`;
|
|
558
|
+
gcode += `G1 X50 Y10\n`;
|
|
559
|
+
gcode += `G1 X50 Y50\n`;
|
|
560
|
+
gcode += `G1 X10 Y50\n`;
|
|
561
|
+
gcode += `G1 X10 Y10\n`;
|
|
562
|
+
return gcode;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Apply retraction moves to G-code
|
|
567
|
+
*/
|
|
568
|
+
function applyRetraction(gcode, settings) {
|
|
569
|
+
const retractDist = settings.retractDistance || 5;
|
|
570
|
+
const retractSpeed = settings.retractSpeed || 40;
|
|
571
|
+
|
|
572
|
+
// Simple: add retraction before rapid moves
|
|
573
|
+
gcode = gcode.replace(/G0\s+Z/g, `G1 E-${retractDist} F${retractSpeed * 60}\nG0 Z`);
|
|
574
|
+
return gcode;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Generate G-code file header
|
|
579
|
+
*/
|
|
580
|
+
function generateHeader() {
|
|
581
|
+
let gcode = '';
|
|
582
|
+
gcode += `;Generated by cycleCAD MachineControl v1.0\n`;
|
|
583
|
+
gcode += `;Machine: ${state.currentMachine.profile.name}\n`;
|
|
584
|
+
gcode += `;Date: ${new Date().toLocaleString()}\n`;
|
|
585
|
+
gcode += `;Flavor: ${state.currentMachine.profile.gcodeFlavor}\n\n`;
|
|
586
|
+
return gcode;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Generate G-code file footer
|
|
591
|
+
*/
|
|
592
|
+
function generateFooter() {
|
|
593
|
+
let gcode = '\n;End of program\n';
|
|
594
|
+
gcode += `M104 S0\n`; // Nozzle off
|
|
595
|
+
gcode += `M140 S0\n`; // Bed off
|
|
596
|
+
gcode += `M107\n`; // Fan off
|
|
597
|
+
gcode += `G28\n`; // Home
|
|
598
|
+
gcode += `M84\n`; // Disable steppers
|
|
599
|
+
return gcode;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ============================================================================
|
|
603
|
+
// 3. SEND G-CODE & CONTROL
|
|
604
|
+
// ============================================================================
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Send G-code command(s) to machine
|
|
608
|
+
* @param {string|string[]} cmd - G-code command or array of commands
|
|
609
|
+
* @param {boolean} queued - Queue for later (true) or send immediately (false)
|
|
610
|
+
* @returns {Promise<string>} Machine response
|
|
611
|
+
*/
|
|
612
|
+
async function sendGCode(cmd, queued = false) {
|
|
613
|
+
if (!state.connected) throw new Error('Not connected to machine');
|
|
614
|
+
|
|
615
|
+
const commands = Array.isArray(cmd) ? cmd : [cmd];
|
|
616
|
+
|
|
617
|
+
if (queued) {
|
|
618
|
+
state.gCodeBuffer.push(...commands);
|
|
619
|
+
return 'Queued ' + commands.length + ' commands';
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let responses = [];
|
|
623
|
+
for (const c of commands) {
|
|
624
|
+
const resp = await sendRawCommand(c);
|
|
625
|
+
responses.push(resp);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return responses.join('\n');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Send raw command via current connection
|
|
633
|
+
*/
|
|
634
|
+
async function sendRawCommand(cmd) {
|
|
635
|
+
const conn = state.currentMachine.connection;
|
|
636
|
+
|
|
637
|
+
if (typeof conn === 'object' && conn.ws) {
|
|
638
|
+
// WebSocket
|
|
639
|
+
conn.ws.send(cmd);
|
|
640
|
+
return 'Sent: ' + cmd;
|
|
641
|
+
} else if (conn.writer) {
|
|
642
|
+
// Serial
|
|
643
|
+
const encoder = new TextEncoder();
|
|
644
|
+
await conn.writer.write(encoder.encode(cmd + '\n'));
|
|
645
|
+
return 'Sent: ' + cmd;
|
|
646
|
+
} else if (conn.type === 'demo') {
|
|
647
|
+
// Demo mode
|
|
648
|
+
return simulateMachineResponse(cmd);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return 'Error: Unknown connection type';
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Simulate machine response (demo mode)
|
|
656
|
+
*/
|
|
657
|
+
function simulateMachineResponse(cmd) {
|
|
658
|
+
const lcCmd = cmd.toLowerCase();
|
|
659
|
+
|
|
660
|
+
if (lcCmd.startsWith('m104')) {
|
|
661
|
+
const match = cmd.match(/s([\d.]+)/i);
|
|
662
|
+
if (match) state.temperatures.nozzleTarget = parseFloat(match[1]);
|
|
663
|
+
return `ok T:25.0/${state.temperatures.nozzleTarget}`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (lcCmd.startsWith('m140')) {
|
|
667
|
+
const match = cmd.match(/s([\d.]+)/i);
|
|
668
|
+
if (match) state.temperatures.bedTarget = parseFloat(match[1]);
|
|
669
|
+
return `ok B:25.0/${state.temperatures.bedTarget}`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (lcCmd.startsWith('m114')) {
|
|
673
|
+
return `X:${state.position.x.toFixed(2)} Y:${state.position.y.toFixed(2)} Z:${state.position.z.toFixed(2)} Count`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return 'ok';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Get current machine status
|
|
681
|
+
* @returns {Object} Status object
|
|
682
|
+
*/
|
|
683
|
+
function getStatus() {
|
|
684
|
+
return {
|
|
685
|
+
connected: state.connected,
|
|
686
|
+
machine: state.currentMachine?.profile.name,
|
|
687
|
+
isRunning: state.isRunning,
|
|
688
|
+
isPaused: state.isPaused,
|
|
689
|
+
position: state.position,
|
|
690
|
+
temperatures: state.temperatures,
|
|
691
|
+
progress: state.isRunning ? (state.currentLayer / state.totalLayers) * 100 : 0,
|
|
692
|
+
currentLayer: state.currentLayer,
|
|
693
|
+
totalLayers: state.totalLayers,
|
|
694
|
+
feedOverride: state.feedOverride,
|
|
695
|
+
materialUsed: state.materialUsed,
|
|
696
|
+
gCodeBufferLength: state.gCodeBuffer.length,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Start printing/milling job
|
|
702
|
+
*/
|
|
703
|
+
function startJob() {
|
|
704
|
+
state.isRunning = true;
|
|
705
|
+
state.isPaused = false;
|
|
706
|
+
state.currentLayer = 0;
|
|
707
|
+
state.startTime = Date.now();
|
|
708
|
+
state.materialUsed = { length: 0, weight: 0 };
|
|
709
|
+
|
|
710
|
+
// Send first batch of G-code
|
|
711
|
+
const batchSize = 50;
|
|
712
|
+
for (let i = 0; i < Math.min(batchSize, state.gCodeBuffer.length); i++) {
|
|
713
|
+
sendRawCommand(state.gCodeBuffer[i]);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Pause current job
|
|
719
|
+
*/
|
|
720
|
+
function pauseJob() {
|
|
721
|
+
state.isPaused = true;
|
|
722
|
+
sendRawCommand('M25'); // Pause (Marlin)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Resume paused job
|
|
727
|
+
*/
|
|
728
|
+
function resumeJob() {
|
|
729
|
+
state.isPaused = false;
|
|
730
|
+
sendRawCommand('M24'); // Resume (Marlin)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Cancel current job
|
|
735
|
+
*/
|
|
736
|
+
function cancelJob() {
|
|
737
|
+
state.isRunning = false;
|
|
738
|
+
state.isPaused = false;
|
|
739
|
+
state.currentLayer = 0;
|
|
740
|
+
sendRawCommand('M112'); // Emergency stop
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Jog axis by distance
|
|
745
|
+
* @param {string} axis - 'x' | 'y' | 'z'
|
|
746
|
+
* @param {number} distance - Distance in mm (positive or negative)
|
|
747
|
+
* @param {number} feedRate - Feed rate (mm/min)
|
|
748
|
+
*/
|
|
749
|
+
async function jogAxis(axis, distance, feedRate = 600) {
|
|
750
|
+
const move = `G1 ${axis.toUpperCase()}${state.position[axis.toLowerCase()] + distance} F${feedRate}`;
|
|
751
|
+
return sendRawCommand(move);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Set nozzle/bed temperature
|
|
756
|
+
*/
|
|
757
|
+
async function setTemperature(type, temp) {
|
|
758
|
+
if (type === 'nozzle') return sendRawCommand(`M104 S${temp}`);
|
|
759
|
+
if (type === 'bed') return sendRawCommand(`M140 S${temp}`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Record temperature history for graphing
|
|
764
|
+
*/
|
|
765
|
+
function recordTempHistory() {
|
|
766
|
+
state.tempHistory.push({
|
|
767
|
+
time: Date.now() - (state.startTime || Date.now()),
|
|
768
|
+
nozzle: state.temperatures.nozzle,
|
|
769
|
+
bed: state.temperatures.bed,
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Keep last 100 records
|
|
773
|
+
if (state.tempHistory.length > 100) state.tempHistory.shift();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ============================================================================
|
|
777
|
+
// 4. UI PANEL
|
|
778
|
+
// ============================================================================
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Get UI panel HTML
|
|
782
|
+
* @returns {HTMLElement} Panel DOM element
|
|
783
|
+
*/
|
|
784
|
+
function getUI() {
|
|
785
|
+
const panel = document.createElement('div');
|
|
786
|
+
panel.className = 'machine-control-panel';
|
|
787
|
+
panel.innerHTML = `
|
|
788
|
+
<div class="machine-tabs">
|
|
789
|
+
<button class="tab-btn active" data-tab="connect">Connect</button>
|
|
790
|
+
<button class="tab-btn" data-tab="prepare">Prepare</button>
|
|
791
|
+
<button class="tab-btn" data-tab="preview">Preview</button>
|
|
792
|
+
<button class="tab-btn" data-tab="control">Control</button>
|
|
793
|
+
<button class="tab-btn" data-tab="monitor">Monitor</button>
|
|
794
|
+
<button class="tab-btn" data-tab="console">Console</button>
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<!-- CONNECT TAB -->
|
|
798
|
+
<div class="tab-content active" id="tab-connect">
|
|
799
|
+
<h3>Machine Connection</h3>
|
|
800
|
+
<div class="machine-profiles">
|
|
801
|
+
${Object.entries(MACHINE_PROFILES).map(([key, profile]) => `
|
|
802
|
+
<button class="profile-btn" data-profile="${key}">
|
|
803
|
+
<strong>${profile.name}</strong><br>
|
|
804
|
+
<small>${profile.type.toUpperCase()} • ${profile.bed.x}×${profile.bed.y}mm</small>
|
|
805
|
+
</button>
|
|
806
|
+
`).join('')}
|
|
807
|
+
</div>
|
|
808
|
+
|
|
809
|
+
<h4>Connection Protocol</h4>
|
|
810
|
+
<div class="protocol-select">
|
|
811
|
+
<label><input type="radio" name="protocol" value="websocket"> WebSocket (Klipper/Duet)</label>
|
|
812
|
+
<label><input type="radio" name="protocol" value="octoprint"> OctoPrint API</label>
|
|
813
|
+
<label><input type="radio" name="protocol" value="serial"> Web Serial (USB)</label>
|
|
814
|
+
<label><input type="radio" name="protocol" value="moonraker"> Moonraker</label>
|
|
815
|
+
<label><input type="radio" name="protocol" value="demo" checked> Demo Mode</label>
|
|
816
|
+
</div>
|
|
817
|
+
|
|
818
|
+
<div class="connect-form">
|
|
819
|
+
<input type="text" placeholder="Host (e.g., 192.168.1.100)" id="host" value="localhost">
|
|
820
|
+
<input type="number" placeholder="Port" id="port" value="5000" min="1" max="65535">
|
|
821
|
+
<input type="password" placeholder="API Key (if needed)" id="apiKey">
|
|
822
|
+
<button id="connect-btn" class="btn-primary">Connect</button>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<div class="status-display">
|
|
826
|
+
<div class="status-led" id="status-led"></div>
|
|
827
|
+
<span id="status-text">Disconnected</span>
|
|
828
|
+
</div>
|
|
829
|
+
|
|
830
|
+
<h4>Connected Machines</h4>
|
|
831
|
+
<div id="machine-list"></div>
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
<!-- PREPARE TAB -->
|
|
835
|
+
<div class="tab-content" id="tab-prepare">
|
|
836
|
+
<h3>G-Code Settings</h3>
|
|
837
|
+
|
|
838
|
+
<div class="setting-group">
|
|
839
|
+
<label>Mode: <select id="mode-select">
|
|
840
|
+
<option value="fdm">FDM Printing</option>
|
|
841
|
+
<option value="cnc_mill">CNC Milling</option>
|
|
842
|
+
<option value="laser">Laser Cutting</option>
|
|
843
|
+
</select></label>
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
<div id="fdm-settings" class="mode-settings">
|
|
847
|
+
<label>Layer Height (mm): <input type="number" id="layer-height" min="0.1" max="0.4" step="0.05" value="0.2"></label>
|
|
848
|
+
<label>Infill (%): <input type="range" id="infill" min="0" max="100" value="20"></label>
|
|
849
|
+
<label>Wall Count: <input type="number" id="wall-count" min="1" max="5" value="2"></label>
|
|
850
|
+
<label>Nozzle Temp (°C): <input type="number" id="nozzle-temp" min="180" max="250" value="200"></label>
|
|
851
|
+
<label>Bed Temp (°C): <input type="number" id="bed-temp" min="20" max="100" value="60"></label>
|
|
852
|
+
<label>Support Type: <select id="support-type">
|
|
853
|
+
<option value="tree">Tree</option>
|
|
854
|
+
<option value="linear">Linear</option>
|
|
855
|
+
<option value="none">None</option>
|
|
856
|
+
</select></label>
|
|
857
|
+
</div>
|
|
858
|
+
|
|
859
|
+
<div id="cnc-settings" class="mode-settings" style="display:none;">
|
|
860
|
+
<label>Feed Rate (mm/min): <input type="number" id="feed-rate" min="100" max="2000" value="600"></label>
|
|
861
|
+
<label>Spindle Speed (RPM): <input type="number" id="spindle-speed" min="500" max="24000" value="5000"></label>
|
|
862
|
+
<label>Depth of Cut (mm): <input type="number" id="depth-cut" min="1" max="20" value="10"></label>
|
|
863
|
+
<label>Step Down (mm): <input type="number" id="step-down" min="1" max="20" value="5"></label>
|
|
864
|
+
</div>
|
|
865
|
+
|
|
866
|
+
<div id="laser-settings" class="mode-settings" style="display:none;">
|
|
867
|
+
<label>Power (%): <input type="range" id="laser-power" min="0" max="100" value="80"></label>
|
|
868
|
+
<label>Cut Speed (mm/min): <input type="number" id="cut-speed" min="10" max="500" value="100"></label>
|
|
869
|
+
<label>Engrave Speed (mm/min): <input type="number" id="engrave-speed" min="50" max="1000" value="300"></label>
|
|
870
|
+
</div>
|
|
871
|
+
|
|
872
|
+
<button id="generate-gcode-btn" class="btn-primary">Generate G-Code</button>
|
|
873
|
+
<div id="gcode-info"></div>
|
|
874
|
+
</div>
|
|
875
|
+
|
|
876
|
+
<!-- PREVIEW TAB -->
|
|
877
|
+
<div class="tab-content" id="tab-preview">
|
|
878
|
+
<h3>Toolpath Preview</h3>
|
|
879
|
+
<canvas id="toolpath-canvas" width="400" height="300"></canvas>
|
|
880
|
+
<div class="preview-controls">
|
|
881
|
+
<label>Layer: <input type="range" id="layer-slider" min="0" max="1" value="0" style="width:200px;"></label>
|
|
882
|
+
<span id="layer-info">Layer 0/1</span>
|
|
883
|
+
</div>
|
|
884
|
+
<div class="preview-stats">
|
|
885
|
+
<p>Estimated Time: <strong id="est-time">--:--</strong></p>
|
|
886
|
+
<p>Material Weight: <strong id="material-weight">0g</strong></p>
|
|
887
|
+
</div>
|
|
888
|
+
</div>
|
|
889
|
+
|
|
890
|
+
<!-- CONTROL TAB -->
|
|
891
|
+
<div class="tab-content" id="tab-control">
|
|
892
|
+
<h3>Machine Control</h3>
|
|
893
|
+
|
|
894
|
+
<h4>Jog Pad</h4>
|
|
895
|
+
<div class="jog-pad">
|
|
896
|
+
<div class="jog-grid">
|
|
897
|
+
<button class="jog-btn" data-axis="y" data-dist="10">+Y</button>
|
|
898
|
+
<button class="jog-btn" data-axis="y" data-dist="1">+Y</button>
|
|
899
|
+
<button class="jog-btn" data-axis="z" data-dist="10">+Z</button>
|
|
900
|
+
</div>
|
|
901
|
+
<div class="jog-grid">
|
|
902
|
+
<button class="jog-btn" data-axis="x" data-dist="-10">-X</button>
|
|
903
|
+
<button class="jog-btn" data-axis="x" data-dist="10">+X</button>
|
|
904
|
+
<button class="jog-btn" data-axis="z" data-dist="-10">-Z</button>
|
|
905
|
+
</div>
|
|
906
|
+
<div class="jog-grid">
|
|
907
|
+
<button class="jog-btn" data-axis="y" data-dist="-10">-Y</button>
|
|
908
|
+
<button class="jog-btn" data-axis="y" data-dist="-1">-Y</button>
|
|
909
|
+
<button class="jog-btn" data-axis="x" data-dist="0.1">0.1mm</button>
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
912
|
+
|
|
913
|
+
<h4>Axis Control</h4>
|
|
914
|
+
<button id="home-all-btn" class="btn-secondary">Home All (G28)</button>
|
|
915
|
+
<button id="set-zero-btn" class="btn-secondary">Set Zero (G92)</button>
|
|
916
|
+
|
|
917
|
+
<h4>Temperature Control</h4>
|
|
918
|
+
<div class="temp-controls">
|
|
919
|
+
<label>Nozzle: <input type="number" id="set-nozzle-temp" min="0" max="300" value="200"></label>
|
|
920
|
+
<button id="set-nozzle-btn" class="btn-secondary">Set</button>
|
|
921
|
+
<label>Bed: <input type="number" id="set-bed-temp" min="0" max="120" value="60"></label>
|
|
922
|
+
<button id="set-bed-btn" class="btn-secondary">Set</button>
|
|
923
|
+
</div>
|
|
924
|
+
|
|
925
|
+
<h4>Extrude/Retract</h4>
|
|
926
|
+
<div class="extrude-controls">
|
|
927
|
+
<input type="number" id="extrude-dist" min="0" max="100" value="10" placeholder="Distance (mm)">
|
|
928
|
+
<button id="extrude-btn" class="btn-secondary">Extrude</button>
|
|
929
|
+
<button id="retract-btn" class="btn-secondary">Retract</button>
|
|
930
|
+
</div>
|
|
931
|
+
|
|
932
|
+
<h4>Overrides</h4>
|
|
933
|
+
<label>Feed Rate: <input type="range" id="feed-override" min="50" max="200" value="100">
|
|
934
|
+
<span id="feed-override-val">100%</span>
|
|
935
|
+
</label>
|
|
936
|
+
<label>Spindle Speed: <input type="range" id="spindle-override" min="50" max="200" value="100">
|
|
937
|
+
<span id="spindle-override-val">100%</span>
|
|
938
|
+
</label>
|
|
939
|
+
|
|
940
|
+
<button id="emergency-stop-btn" class="btn-danger">EMERGENCY STOP (M112)</button>
|
|
941
|
+
</div>
|
|
942
|
+
|
|
943
|
+
<!-- MONITOR TAB -->
|
|
944
|
+
<div class="tab-content" id="tab-monitor">
|
|
945
|
+
<h3>Job Monitor</h3>
|
|
946
|
+
|
|
947
|
+
<div class="progress-display">
|
|
948
|
+
<div class="progress-bar">
|
|
949
|
+
<div id="progress-fill" style="width:0%"></div>
|
|
950
|
+
</div>
|
|
951
|
+
<span id="progress-text">0%</span>
|
|
952
|
+
</div>
|
|
953
|
+
|
|
954
|
+
<div class="monitor-stats">
|
|
955
|
+
<p>Layer: <strong id="monitor-layer">0/1</strong></p>
|
|
956
|
+
<p>Time: <strong id="monitor-time">00:00</strong> / <strong id="monitor-eta">--:--</strong></p>
|
|
957
|
+
<p>Material: <strong id="monitor-material">0m</strong> (<strong id="monitor-weight">0g</strong>)</p>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
<div class="temp-chart">
|
|
961
|
+
<canvas id="temp-chart" width="300" height="150"></canvas>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
<div class="job-controls">
|
|
965
|
+
<button id="start-btn" class="btn-primary">Start Print</button>
|
|
966
|
+
<button id="pause-btn" class="btn-secondary" disabled>Pause</button>
|
|
967
|
+
<button id="resume-btn" class="btn-secondary" disabled>Resume</button>
|
|
968
|
+
<button id="cancel-btn" class="btn-danger">Cancel</button>
|
|
969
|
+
</div>
|
|
970
|
+
|
|
971
|
+
<h4>Print History</h4>
|
|
972
|
+
<div id="job-history"></div>
|
|
973
|
+
</div>
|
|
974
|
+
|
|
975
|
+
<!-- CONSOLE TAB -->
|
|
976
|
+
<div class="tab-content" id="tab-console">
|
|
977
|
+
<h3>G-Code Console</h3>
|
|
978
|
+
<textarea id="console-output" readonly style="width:100%; height:150px; font-family:monospace; background:#1e1e1e; color:#00ff00; padding:8px;"></textarea>
|
|
979
|
+
<div class="console-input">
|
|
980
|
+
<input type="text" id="gcode-input" placeholder="Enter G-code command..." style="width:calc(100% - 80px);">
|
|
981
|
+
<button id="send-gcode-btn" class="btn-secondary">Send</button>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
|
|
986
|
+
<style>
|
|
987
|
+
.machine-control-panel { padding: 12px; font-size: 0.85rem; }
|
|
988
|
+
|
|
989
|
+
.machine-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 12px; }
|
|
990
|
+
.tab-btn { padding: 6px 12px; background: transparent; border: none; cursor: pointer; border-bottom: 2px solid transparent; }
|
|
991
|
+
.tab-btn.active { border-bottom-color: var(--accent); color: var(--accent); }
|
|
992
|
+
|
|
993
|
+
.tab-content { display: none; }
|
|
994
|
+
.tab-content.active { display: block; }
|
|
995
|
+
|
|
996
|
+
.machine-profiles { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
|
|
997
|
+
.profile-btn { padding: 8px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; text-align: left; }
|
|
998
|
+
.profile-btn:hover { background: var(--bg-tertiary); }
|
|
999
|
+
|
|
1000
|
+
.protocol-select { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
|
1001
|
+
.protocol-select label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
1002
|
+
|
|
1003
|
+
.connect-form { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
|
1004
|
+
.connect-form input { padding: 6px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; color: inherit; }
|
|
1005
|
+
|
|
1006
|
+
.status-display { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
|
|
1007
|
+
.status-led { width: 12px; height: 12px; border-radius: 50%; background: #ff4444; }
|
|
1008
|
+
.status-led.connected { background: #44ff44; }
|
|
1009
|
+
|
|
1010
|
+
.machine-list { background: var(--bg-secondary); border-radius: 4px; padding: 8px; }
|
|
1011
|
+
|
|
1012
|
+
.setting-group { margin-bottom: 12px; }
|
|
1013
|
+
.setting-group label { display: flex; align-items: center; gap: 8px; }
|
|
1014
|
+
.setting-group select, .setting-group input { padding: 4px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 3px; color: inherit; }
|
|
1015
|
+
|
|
1016
|
+
.mode-settings { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
|
1017
|
+
.mode-settings label { display: flex; align-items: center; justify-content: space-between; }
|
|
1018
|
+
|
|
1019
|
+
.jog-pad { display: grid; grid-template-columns: repeat(3, 60px); gap: 4px; margin-bottom: 12px; }
|
|
1020
|
+
.jog-grid { display: contents; }
|
|
1021
|
+
.jog-btn { padding: 8px; background: var(--accent); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 0.75rem; font-weight: bold; }
|
|
1022
|
+
.jog-btn:hover { opacity: 0.8; }
|
|
1023
|
+
|
|
1024
|
+
.temp-controls { display: grid; grid-template-columns: 1fr 80px; gap: 8px; align-items: center; margin-bottom: 12px; }
|
|
1025
|
+
.extrude-controls { display: flex; gap: 4px; margin-bottom: 12px; }
|
|
1026
|
+
|
|
1027
|
+
.progress-bar { width: 100%; height: 24px; background: var(--bg-secondary); border-radius: 4px; overflow: hidden; margin-bottom: 8px; }
|
|
1028
|
+
#progress-fill { height: 100%; background: var(--accent); transition: width 0.1s; }
|
|
1029
|
+
|
|
1030
|
+
.monitor-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
|
|
1031
|
+
.monitor-stats p { margin: 4px 0; }
|
|
1032
|
+
|
|
1033
|
+
.job-controls { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
1034
|
+
|
|
1035
|
+
.btn-primary { padding: 8px 12px; background: var(--accent); color: white; border: none; border-radius: 4px; cursor: pointer; }
|
|
1036
|
+
.btn-secondary { padding: 6px 10px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; }
|
|
1037
|
+
.btn-danger { padding: 8px 12px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
|
|
1038
|
+
|
|
1039
|
+
.btn-primary:hover { opacity: 0.9; }
|
|
1040
|
+
.btn-secondary:hover { background: var(--bg-tertiary); }
|
|
1041
|
+
.btn-danger:hover { background: #ff2222; }
|
|
1042
|
+
</style>
|
|
1043
|
+
`;
|
|
1044
|
+
|
|
1045
|
+
// Attach event listeners
|
|
1046
|
+
attachPanelListeners(panel);
|
|
1047
|
+
|
|
1048
|
+
return panel;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Attach all event listeners to panel
|
|
1053
|
+
*/
|
|
1054
|
+
function attachPanelListeners(panel) {
|
|
1055
|
+
// Tab switching
|
|
1056
|
+
panel.querySelectorAll('.tab-btn').forEach(btn => {
|
|
1057
|
+
btn.addEventListener('click', (e) => {
|
|
1058
|
+
panel.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
1059
|
+
panel.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
1060
|
+
e.target.classList.add('active');
|
|
1061
|
+
const tabName = e.target.getAttribute('data-tab');
|
|
1062
|
+
panel.querySelector('#tab-' + tabName).classList.add('active');
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
// Profile selection
|
|
1067
|
+
panel.querySelectorAll('.profile-btn').forEach(btn => {
|
|
1068
|
+
btn.addEventListener('click', (e) => {
|
|
1069
|
+
state.activeProfile = e.currentTarget.getAttribute('data-profile');
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
// Connect button
|
|
1074
|
+
panel.querySelector('#connect-btn').addEventListener('click', async () => {
|
|
1075
|
+
const protocol = panel.querySelector('input[name="protocol"]:checked').value;
|
|
1076
|
+
const host = panel.querySelector('#host').value;
|
|
1077
|
+
const port = parseInt(panel.querySelector('#port').value);
|
|
1078
|
+
const apiKey = panel.querySelector('#apiKey').value;
|
|
1079
|
+
const profileKey = state.activeProfile || 'ender3';
|
|
1080
|
+
|
|
1081
|
+
const success = await connect({ protocol, host, port, apiKey, profileKey });
|
|
1082
|
+
const statusLed = panel.querySelector('#status-led');
|
|
1083
|
+
const statusText = panel.querySelector('#status-text');
|
|
1084
|
+
|
|
1085
|
+
if (success) {
|
|
1086
|
+
statusLed.classList.add('connected');
|
|
1087
|
+
statusText.textContent = 'Connected to ' + MACHINE_PROFILES[profileKey].name;
|
|
1088
|
+
} else {
|
|
1089
|
+
statusLed.classList.remove('connected');
|
|
1090
|
+
statusText.textContent = 'Connection failed';
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
updateMachineList(panel);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Mode select
|
|
1097
|
+
panel.querySelector('#mode-select').addEventListener('change', (e) => {
|
|
1098
|
+
const mode = e.target.value;
|
|
1099
|
+
panel.querySelector('#fdm-settings').style.display = mode === 'fdm' ? 'flex' : 'none';
|
|
1100
|
+
panel.querySelector('#cnc-settings').style.display = mode === 'cnc_mill' ? 'flex' : 'none';
|
|
1101
|
+
panel.querySelector('#laser-settings').style.display = mode === 'laser' ? 'flex' : 'none';
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
// Generate G-code
|
|
1105
|
+
panel.querySelector('#generate-gcode-btn').addEventListener('click', () => {
|
|
1106
|
+
const mode = panel.querySelector('#mode-select').value;
|
|
1107
|
+
const settings = {
|
|
1108
|
+
mode,
|
|
1109
|
+
layerHeight: parseFloat(panel.querySelector('#layer-height').value),
|
|
1110
|
+
infillPercent: parseInt(panel.querySelector('#infill').value),
|
|
1111
|
+
wallCount: parseInt(panel.querySelector('#wall-count').value),
|
|
1112
|
+
feedRate: parseInt(panel.querySelector('#feed-rate').value),
|
|
1113
|
+
power: parseInt(panel.querySelector('#laser-power').value),
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// Get dummy geometry (in real use, would come from 3D model)
|
|
1117
|
+
const geometry = new THREE.BoxGeometry(50, 50, 50);
|
|
1118
|
+
|
|
1119
|
+
const gcode = generateToolpath(geometry, settings);
|
|
1120
|
+
panel.querySelector('#gcode-info').innerHTML = `<p style="color:var(--accent);">Generated ${state.gCodeBuffer.length} lines of G-code</p>`;
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
// Jog buttons
|
|
1124
|
+
panel.querySelectorAll('.jog-btn').forEach(btn => {
|
|
1125
|
+
btn.addEventListener('click', async (e) => {
|
|
1126
|
+
const axis = e.target.getAttribute('data-axis');
|
|
1127
|
+
const dist = parseFloat(e.target.getAttribute('data-dist'));
|
|
1128
|
+
await jogAxis(axis, dist);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// Temperature controls
|
|
1133
|
+
panel.querySelector('#set-nozzle-btn').addEventListener('click', () => {
|
|
1134
|
+
const temp = parseInt(panel.querySelector('#set-nozzle-temp').value);
|
|
1135
|
+
setTemperature('nozzle', temp);
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
panel.querySelector('#set-bed-btn').addEventListener('click', () => {
|
|
1139
|
+
const temp = parseInt(panel.querySelector('#set-bed-temp').value);
|
|
1140
|
+
setTemperature('bed', temp);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
// Extrude/Retract
|
|
1144
|
+
panel.querySelector('#extrude-btn').addEventListener('click', () => {
|
|
1145
|
+
const dist = parseFloat(panel.querySelector('#extrude-dist').value);
|
|
1146
|
+
sendRawCommand(`G1 E${dist} F200`);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
panel.querySelector('#retract-btn').addEventListener('click', () => {
|
|
1150
|
+
const dist = parseFloat(panel.querySelector('#extrude-dist').value);
|
|
1151
|
+
sendRawCommand(`G1 E-${dist} F200`);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// Overrides
|
|
1155
|
+
panel.querySelector('#feed-override').addEventListener('input', (e) => {
|
|
1156
|
+
state.feedOverride = parseInt(e.target.value);
|
|
1157
|
+
panel.querySelector('#feed-override-val').textContent = state.feedOverride + '%';
|
|
1158
|
+
sendRawCommand(`M220 S${state.feedOverride}`);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
panel.querySelector('#spindle-override').addEventListener('input', (e) => {
|
|
1162
|
+
state.spindleOverride = parseInt(e.target.value);
|
|
1163
|
+
panel.querySelector('#spindle-override-val').textContent = state.spindleOverride + '%';
|
|
1164
|
+
sendRawCommand(`M221 S${state.spindleOverride}`);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
// Emergency stop
|
|
1168
|
+
panel.querySelector('#emergency-stop-btn').addEventListener('click', () => {
|
|
1169
|
+
sendRawCommand('M112');
|
|
1170
|
+
cancelJob();
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// Job controls
|
|
1174
|
+
panel.querySelector('#start-btn').addEventListener('click', startJob);
|
|
1175
|
+
panel.querySelector('#pause-btn').addEventListener('click', pauseJob);
|
|
1176
|
+
panel.querySelector('#resume-btn').addEventListener('click', resumeJob);
|
|
1177
|
+
panel.querySelector('#cancel-btn').addEventListener('click', cancelJob);
|
|
1178
|
+
|
|
1179
|
+
// Console
|
|
1180
|
+
panel.querySelector('#send-gcode-btn').addEventListener('click', async () => {
|
|
1181
|
+
const cmd = panel.querySelector('#gcode-input').value.trim();
|
|
1182
|
+
if (cmd) {
|
|
1183
|
+
const resp = await sendGCode(cmd);
|
|
1184
|
+
const output = panel.querySelector('#console-output');
|
|
1185
|
+
output.value += `> ${cmd}\n${resp}\n`;
|
|
1186
|
+
output.scrollTop = output.scrollHeight;
|
|
1187
|
+
panel.querySelector('#gcode-input').value = '';
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
// Home all
|
|
1192
|
+
panel.querySelector('#home-all-btn').addEventListener('click', () => sendRawCommand('G28'));
|
|
1193
|
+
|
|
1194
|
+
// Set zero
|
|
1195
|
+
panel.querySelector('#set-zero-btn').addEventListener('click', () => sendRawCommand('G92 X0 Y0 Z0'));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Update machine list display
|
|
1200
|
+
*/
|
|
1201
|
+
function updateMachineList(panel) {
|
|
1202
|
+
const list = panel.querySelector('#machine-list');
|
|
1203
|
+
list.innerHTML = state.machines.map((m, idx) => `
|
|
1204
|
+
<div style="padding:8px; background:var(--bg-tertiary); margin:4px 0; border-radius:3px;">
|
|
1205
|
+
<strong>${m.name}</strong><br>
|
|
1206
|
+
<small>${m.protocol} • ${m.config.host}:${m.config.port}</small>
|
|
1207
|
+
</div>
|
|
1208
|
+
`).join('');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Update OctoPrint status from WebSocket message
|
|
1213
|
+
*/
|
|
1214
|
+
function updateOctoPrintStatus(msg) {
|
|
1215
|
+
if (msg.state === 'Printing') state.isRunning = true;
|
|
1216
|
+
if (msg.state === 'Paused') state.isPaused = true;
|
|
1217
|
+
parseResponse(msg.temps);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Update Moonraker status from notify_update
|
|
1222
|
+
*/
|
|
1223
|
+
function updateMoonrakerStatus(params) {
|
|
1224
|
+
if (params[0].gcode_move) {
|
|
1225
|
+
const pos = params[0].gcode_move.gcode_position;
|
|
1226
|
+
state.position = { x: pos[0], y: pos[1], z: pos[2] };
|
|
1227
|
+
}
|
|
1228
|
+
if (params[0].extruder) {
|
|
1229
|
+
state.temperatures.nozzle = params[0].extruder.temperature;
|
|
1230
|
+
state.temperatures.nozzleTarget = params[0].extruder.target;
|
|
1231
|
+
}
|
|
1232
|
+
if (params[0].heater_bed) {
|
|
1233
|
+
state.temperatures.bed = params[0].heater_bed.temperature;
|
|
1234
|
+
state.temperatures.bedTarget = params[0].heater_bed.target;
|
|
1235
|
+
}
|
|
1236
|
+
recordTempHistory();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// ============================================================================
|
|
1240
|
+
// PUBLIC API
|
|
1241
|
+
// ============================================================================
|
|
1242
|
+
|
|
1243
|
+
return {
|
|
1244
|
+
init: () => {
|
|
1245
|
+
console.log('[MachineControl] Initialized');
|
|
1246
|
+
},
|
|
1247
|
+
getUI,
|
|
1248
|
+
execute: (cmd, params) => {
|
|
1249
|
+
console.log('[MachineControl] Execute:', cmd, params);
|
|
1250
|
+
},
|
|
1251
|
+
connect,
|
|
1252
|
+
sendGCode,
|
|
1253
|
+
getStatus,
|
|
1254
|
+
generateToolpath,
|
|
1255
|
+
startJob,
|
|
1256
|
+
pauseJob,
|
|
1257
|
+
resumeJob,
|
|
1258
|
+
cancelJob,
|
|
1259
|
+
jogAxis,
|
|
1260
|
+
setTemperature,
|
|
1261
|
+
};
|
|
1262
|
+
})();
|
|
1263
|
+
|
|
1264
|
+
// ============================================================================
|
|
1265
|
+
// EXPORT
|
|
1266
|
+
// ============================================================================
|
|
1267
|
+
|
|
1268
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1269
|
+
module.exports = window.CycleCAD.MachineControl;
|
|
1270
|
+
}
|