cyclecad 2.0.1 → 2.1.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.
Files changed (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,1067 @@
1
+ /**
2
+ * @file cam-module.js
3
+ * @description CAM (Computer-Aided Manufacturing) Module.
4
+ * Generates toolpaths for CNC milling, turning, and 3D printing.
5
+ * Includes tool library, G-code generation, and toolpath simulation.
6
+ *
7
+ * @version 1.0.0
8
+ * @author Sachin Kumar <vvlars@googlemail.com>
9
+ * @license MIT
10
+ * @module cam
11
+ * @requires viewport, operations
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ /**
17
+ * CAM (Computer-Aided Manufacturing) Module
18
+ * Handles toolpath generation, G-code output, and manufacturing simulation.
19
+ */
20
+ const CAMModule = (() => {
21
+ const MODULE_NAME = 'cam';
22
+ let viewport = null;
23
+ let scene = null;
24
+ let ui = null;
25
+
26
+ // Tool library with standard tools
27
+ const defaultToolLibrary = {
28
+ 'flat-endmill-6mm': {
29
+ id: 'flat-endmill-6mm',
30
+ name: 'Flat End Mill 6mm',
31
+ type: 'flat',
32
+ diameter: 6,
33
+ fluteLength: 20,
34
+ overallLength: 50,
35
+ material: 'carbide',
36
+ coating: 'TiN',
37
+ rpm: 12000,
38
+ feed: 1200,
39
+ chipLoad: 0.1,
40
+ cost: 15.50,
41
+ },
42
+ 'ball-endmill-3mm': {
43
+ id: 'ball-endmill-3mm',
44
+ name: 'Ball End Mill 3mm',
45
+ type: 'ball',
46
+ diameter: 3,
47
+ fluteLength: 15,
48
+ overallLength: 45,
49
+ material: 'carbide',
50
+ coating: 'TiN',
51
+ rpm: 18000,
52
+ feed: 800,
53
+ chipLoad: 0.08,
54
+ cost: 12.75,
55
+ },
56
+ 'drill-5mm': {
57
+ id: 'drill-5mm',
58
+ name: 'Drill 5mm',
59
+ type: 'drill',
60
+ diameter: 5,
61
+ pointAngle: 118,
62
+ fluteLength: 30,
63
+ overallLength: 70,
64
+ material: 'HSS',
65
+ rpm: 3000,
66
+ feed: 200,
67
+ chipLoad: 0.15,
68
+ cost: 2.50,
69
+ },
70
+ 'face-mill-50mm': {
71
+ id: 'face-mill-50mm',
72
+ name: 'Face Mill 50mm',
73
+ type: 'face',
74
+ diameter: 50,
75
+ inserts: 5,
76
+ material: 'carbide',
77
+ rpm: 4000,
78
+ feed: 2000,
79
+ chipLoad: 0.2,
80
+ cost: 85.00,
81
+ },
82
+ 'slot-drills-4mm': {
83
+ id: 'slot-drills-4mm',
84
+ name: 'Slot Drill 4mm',
85
+ type: 'slot',
86
+ diameter: 4,
87
+ fluteLength: 12,
88
+ overallLength: 40,
89
+ material: 'carbide',
90
+ rpm: 15000,
91
+ feed: 900,
92
+ chipLoad: 0.12,
93
+ cost: 11.25,
94
+ },
95
+ 'chamfer-90deg': {
96
+ id: 'chamfer-90deg',
97
+ name: 'Chamfer 90°',
98
+ type: 'chamfer',
99
+ diameter: 10,
100
+ angle: 90,
101
+ material: 'carbide',
102
+ rpm: 8000,
103
+ feed: 600,
104
+ cost: 18.50,
105
+ },
106
+ };
107
+
108
+ // CAM state
109
+ const camState = {
110
+ workCoordinateSystem: null,
111
+ stock: null,
112
+ selectedTool: null,
113
+ toolLibrary: new Map(Object.entries(defaultToolLibrary)),
114
+ toolpaths: new Map(),
115
+ gcode: null,
116
+ setupParams: {
117
+ feedUnits: 'inch/min', // inch/min | mm/min
118
+ rapidRate: 5000,
119
+ safeHeight: 5,
120
+ retractHeight: 10,
121
+ spindleDirection: 'cw', // cw | ccw
122
+ },
123
+ };
124
+
125
+ const toolpathCounter = { count: 0 };
126
+
127
+ /**
128
+ * Initialize the CAM Module
129
+ * @param {Object} deps - Dependencies { viewport, scene }
130
+ */
131
+ function init(deps) {
132
+ viewport = deps.viewport;
133
+ scene = deps.scene;
134
+ registerCommands();
135
+ window.addEventListener('keydown', handleKeyboard);
136
+ }
137
+
138
+ /**
139
+ * Define work coordinate system and stock
140
+ * @param {Object} params
141
+ * @param {string} params.stockType - 'box' | 'cylinder' | 'from_model'
142
+ * @param {Object} params.dimensions - { x, y, z } or { diameter, height }
143
+ * @param {THREE.Vector3} params.origin - WCS origin
144
+ * @param {THREE.Vector3} params.zDir - Z axis (spindle) direction
145
+ * @returns {Object} Setup result
146
+ */
147
+ function setupWorkCoordinateSystem(params = {}) {
148
+ const {
149
+ stockType = 'box',
150
+ dimensions = { x: 100, y: 100, z: 50 },
151
+ origin = new THREE.Vector3(0, 0, 0),
152
+ zDir = new THREE.Vector3(0, 0, 1),
153
+ } = params;
154
+
155
+ // Create WCS frame
156
+ camState.workCoordinateSystem = {
157
+ origin: origin.clone(),
158
+ zDir: zDir.normalize(),
159
+ xDir: new THREE.Vector3(1, 0, 0),
160
+ yDir: new THREE.Vector3(0, 1, 0),
161
+ type: stockType,
162
+ dimensions,
163
+ };
164
+
165
+ // Create stock visualization
166
+ const stockGeom = createStockGeometry(stockType, dimensions);
167
+ const stockMat = new THREE.MeshPhongMaterial({
168
+ color: 0xcccccc,
169
+ transparent: true,
170
+ opacity: 0.2,
171
+ wireframe: true,
172
+ });
173
+ const stockMesh = new THREE.Mesh(stockGeom, stockMat);
174
+ stockMesh.position.copy(origin);
175
+ stockMesh.name = 'stock_visualization';
176
+
177
+ camState.stock = stockMesh;
178
+ if (viewport?.scene) {
179
+ viewport.scene.add(stockMesh);
180
+ }
181
+
182
+ console.log('[CAM] WCS setup:', camState.workCoordinateSystem);
183
+ window.dispatchEvent(new CustomEvent('cam:setupComplete', { detail: camState.workCoordinateSystem }));
184
+
185
+ return { status: 'ok', wcs: camState.workCoordinateSystem };
186
+ }
187
+
188
+ /**
189
+ * Generate 2D contour (profile) toolpath
190
+ * @param {Object} params
191
+ * @param {THREE.Vector3[]} params.profile - Profile points (closed loop)
192
+ * @param {number} params.depth - Cut depth
193
+ * @param {string} params.toolId - Tool ID
194
+ * @param {string} params.type - 'inside' | 'outside' | 'on'
195
+ * @param {number} params.stepDown - Depth per pass
196
+ * @returns {Object} Toolpath object
197
+ */
198
+ function generateContour2D(params = {}) {
199
+ const {
200
+ profile = [],
201
+ depth = 10,
202
+ toolId = 'flat-endmill-6mm',
203
+ type = 'outside',
204
+ stepDown = 5,
205
+ } = params;
206
+
207
+ const tool = camState.toolLibrary.get(toolId);
208
+ if (!tool) throw new Error(`Tool ${toolId} not found`);
209
+
210
+ const id = `tp_contour2d_${toolpathCounter.count++}`;
211
+
212
+ // Generate passes
213
+ const passes = [];
214
+ const depthPasses = Math.ceil(depth / stepDown);
215
+
216
+ for (let pass = 0; pass < depthPasses; pass++) {
217
+ const currentDepth = Math.min((pass + 1) * stepDown, depth);
218
+ passes.push({
219
+ depth: currentDepth,
220
+ points: offsetProfile(profile, type === 'inside' ? -tool.diameter / 2 : tool.diameter / 2),
221
+ });
222
+ }
223
+
224
+ const toolpath = {
225
+ id,
226
+ type: 'contour_2d',
227
+ tool,
228
+ profile,
229
+ depth,
230
+ stepDown,
231
+ passes,
232
+ estimatedTime: calculateEstimatedTime(profile, passes, tool),
233
+ status: 'generated',
234
+ };
235
+
236
+ camState.toolpaths.set(id, toolpath);
237
+
238
+ // Visualize toolpath
239
+ visualizeToolpath(toolpath);
240
+
241
+ console.log('[CAM] Contour 2D generated:', { id, passes: passes.length, depth });
242
+ window.dispatchEvent(new CustomEvent('cam:toolpathGenerated', { detail: toolpath }));
243
+
244
+ return { id, type: 'contour_2d', passes: passes.length, estimatedTime: toolpath.estimatedTime };
245
+ }
246
+
247
+ /**
248
+ * Generate pocket (enclosed region clear) toolpath
249
+ * @param {Object} params
250
+ * @param {THREE.Vector3[]} params.region - Region boundary
251
+ * @param {number} params.depth - Pocket depth
252
+ * @param {string} params.toolId - Tool ID
253
+ * @param {number} params.stepDown - Depth per pass
254
+ * @param {number} params.stepOver - Horizontal feed per pass
255
+ * @returns {Object} Toolpath object
256
+ */
257
+ function generatePocket(params = {}) {
258
+ const {
259
+ region = [],
260
+ depth = 10,
261
+ toolId = 'flat-endmill-6mm',
262
+ stepDown = 5,
263
+ stepOver = 3,
264
+ } = params;
265
+
266
+ const tool = camState.toolLibrary.get(toolId);
267
+ if (!tool) throw new Error(`Tool ${toolId} not found`);
268
+
269
+ const id = `tp_pocket_${toolpathCounter.count++}`;
270
+
271
+ // Generate spiral/raster pattern
272
+ const passes = [];
273
+ const depthPasses = Math.ceil(depth / stepDown);
274
+
275
+ for (let dPass = 0; dPass < depthPasses; dPass++) {
276
+ const currentDepth = Math.min((dPass + 1) * stepDown, depth);
277
+ const spiralLines = generateSpiralPattern(region, stepOver);
278
+ passes.push({
279
+ depth: currentDepth,
280
+ lines: spiralLines,
281
+ });
282
+ }
283
+
284
+ const toolpath = {
285
+ id,
286
+ type: 'pocket',
287
+ tool,
288
+ region,
289
+ depth,
290
+ stepDown,
291
+ stepOver,
292
+ passes,
293
+ estimatedTime: calculateEstimatedTime(region, passes, tool),
294
+ status: 'generated',
295
+ };
296
+
297
+ camState.toolpaths.set(id, toolpath);
298
+ visualizeToolpath(toolpath);
299
+
300
+ console.log('[CAM] Pocket generated:', { id, passes: passes.length });
301
+ window.dispatchEvent(new CustomEvent('cam:toolpathGenerated', { detail: toolpath }));
302
+
303
+ return { id, type: 'pocket', passes: passes.length, estimatedTime: toolpath.estimatedTime };
304
+ }
305
+
306
+ /**
307
+ * Generate drilling toolpath
308
+ * @param {Object} params
309
+ * @param {THREE.Vector3[]} params.points - Drill points
310
+ * @param {number} params.depth - Drill depth
311
+ * @param {string} params.toolId - Tool ID
312
+ * @param {string} params.cycle - 'peck' | 'standard' | 'chip_break'
313
+ * @param {number} params.peckDepth - Peck depth for peck drilling
314
+ * @returns {Object} Toolpath object
315
+ */
316
+ function generateDrilling(params = {}) {
317
+ const {
318
+ points = [],
319
+ depth = 10,
320
+ toolId = 'drill-5mm',
321
+ cycle = 'peck',
322
+ peckDepth = 5,
323
+ } = params;
324
+
325
+ const tool = camState.toolLibrary.get(toolId);
326
+ if (!tool) throw new Error(`Tool ${toolId} not found`);
327
+
328
+ const id = `tp_drill_${toolpathCounter.count++}`;
329
+
330
+ // Generate peck pattern if requested
331
+ let drillSequence = points;
332
+ if (cycle === 'peck') {
333
+ drillSequence = points.flatMap(pt => ({
334
+ point: pt,
335
+ pecks: Math.ceil(depth / peckDepth),
336
+ peckDepth,
337
+ }));
338
+ }
339
+
340
+ const toolpath = {
341
+ id,
342
+ type: 'drilling',
343
+ tool,
344
+ points,
345
+ depth,
346
+ cycle,
347
+ drillSequence,
348
+ estimatedTime: calculateDrillingTime(points, depth, tool),
349
+ status: 'generated',
350
+ };
351
+
352
+ camState.toolpaths.set(id, toolpath);
353
+ visualizeToolpath(toolpath);
354
+
355
+ console.log('[CAM] Drilling generated:', { id, points: points.length });
356
+ window.dispatchEvent(new CustomEvent('cam:toolpathGenerated', { detail: toolpath }));
357
+
358
+ return { id, type: 'drilling', points: points.length, estimatedTime: toolpath.estimatedTime };
359
+ }
360
+
361
+ /**
362
+ * Generate face milling toolpath
363
+ * @param {Object} params
364
+ * @param {THREE.Vector3[]} params.region - Face region
365
+ * @param {number} params.depth - Cut depth
366
+ * @param {string} params.toolId - Tool ID
367
+ * @param {number} params.stepOver - Feed per pass
368
+ * @returns {Object} Toolpath object
369
+ */
370
+ function generateFace(params = {}) {
371
+ const {
372
+ region = [],
373
+ depth = 2,
374
+ toolId = 'face-mill-50mm',
375
+ stepOver = 10,
376
+ } = params;
377
+
378
+ const tool = camState.toolLibrary.get(toolId);
379
+ if (!tool) throw new Error(`Tool ${toolId} not found`);
380
+
381
+ const id = `tp_face_${toolpathCounter.count++}`;
382
+
383
+ // Generate raster pattern
384
+ const passes = generateRasterPattern(region, stepOver);
385
+
386
+ const toolpath = {
387
+ id,
388
+ type: 'face',
389
+ tool,
390
+ region,
391
+ depth,
392
+ stepOver,
393
+ passes,
394
+ estimatedTime: calculateEstimatedTime(region, passes, tool),
395
+ status: 'generated',
396
+ };
397
+
398
+ camState.toolpaths.set(id, toolpath);
399
+ visualizeToolpath(toolpath);
400
+
401
+ console.log('[CAM] Face milling generated:', { id, passes: passes.length });
402
+ window.dispatchEvent(new CustomEvent('cam:toolpathGenerated', { detail: toolpath }));
403
+
404
+ return { id, type: 'face', passes: passes.length, estimatedTime: toolpath.estimatedTime };
405
+ }
406
+
407
+ /**
408
+ * Generate adaptive clearing (high-speed roughing)
409
+ * @param {Object} params
410
+ * @param {THREE.Vector3[]} params.region - Region to clear
411
+ * @param {number} params.depth - Clear depth
412
+ * @param {string} params.toolId - Tool ID
413
+ * @param {number} params.stepOver - Horizontal feed
414
+ * @returns {Object} Toolpath object
415
+ */
416
+ function generateAdaptiveClearing(params = {}) {
417
+ const {
418
+ region = [],
419
+ depth = 20,
420
+ toolId = 'flat-endmill-6mm',
421
+ stepOver = 4,
422
+ } = params;
423
+
424
+ const tool = camState.toolLibrary.get(toolId);
425
+ if (!tool) throw new Error(`Tool ${toolId} not found`);
426
+
427
+ const id = `tp_adaptive_${toolpathCounter.count++}`;
428
+
429
+ // Adaptive clearing: constant chip load, variable engagement
430
+ const passes = [];
431
+ const depthPass = Math.min(tool.diameter * 0.75, depth); // engage to 75% dia
432
+ const numPasses = Math.ceil(depth / depthPass);
433
+
434
+ for (let i = 0; i < numPasses; i++) {
435
+ const currentDepth = Math.min((i + 1) * depthPass, depth);
436
+ passes.push({
437
+ depth: currentDepth,
438
+ pattern: generateAdaptivePattern(region, stepOver, currentDepth),
439
+ });
440
+ }
441
+
442
+ const toolpath = {
443
+ id,
444
+ type: 'adaptive_clearing',
445
+ tool,
446
+ region,
447
+ depth,
448
+ stepOver,
449
+ passes,
450
+ estimatedTime: calculateEstimatedTime(region, passes, tool),
451
+ status: 'generated',
452
+ };
453
+
454
+ camState.toolpaths.set(id, toolpath);
455
+ visualizeToolpath(toolpath);
456
+
457
+ console.log('[CAM] Adaptive clearing generated:', { id, passes: passes.length });
458
+ window.dispatchEvent(new CustomEvent('cam:toolpathGenerated', { detail: toolpath }));
459
+
460
+ return { id, type: 'adaptive_clearing', passes: passes.length, estimatedTime: toolpath.estimatedTime };
461
+ }
462
+
463
+ /**
464
+ * Generate parallel finishing toolpath
465
+ * @param {Object} params
466
+ * @param {THREE.BufferGeometry} params.geometry - Surface geometry
467
+ * @param {string} params.toolId - Tool ID
468
+ * @param {number} params.stepOver - Horizontal step-over
469
+ * @param {string} params.direction - 'x' | 'y' | 'diagonal'
470
+ * @returns {Object} Toolpath object
471
+ */
472
+ function generateParallel(params = {}) {
473
+ const {
474
+ geometry = null,
475
+ toolId = 'ball-endmill-3mm',
476
+ stepOver = 2,
477
+ direction = 'x',
478
+ } = params;
479
+
480
+ const tool = camState.toolLibrary.get(toolId);
481
+ if (!tool) throw new Error(`Tool ${toolId} not found`);
482
+
483
+ const id = `tp_parallel_${toolpathCounter.count++}`;
484
+
485
+ const passes = generateParallelPasses(geometry, stepOver, direction);
486
+
487
+ const toolpath = {
488
+ id,
489
+ type: 'parallel',
490
+ tool,
491
+ geometry,
492
+ stepOver,
493
+ direction,
494
+ passes,
495
+ estimatedTime: calculateEstimatedTime([], passes, tool),
496
+ status: 'generated',
497
+ };
498
+
499
+ camState.toolpaths.set(id, toolpath);
500
+ visualizeToolpath(toolpath);
501
+
502
+ console.log('[CAM] Parallel finishing generated:', { id, passes: passes.length });
503
+ window.dispatchEvent(new CustomEvent('cam:toolpathGenerated', { detail: toolpath }));
504
+
505
+ return { id, type: 'parallel', passes: passes.length, estimatedTime: toolpath.estimatedTime };
506
+ }
507
+
508
+ /**
509
+ * Generate FDM slicing (additive/3D printing)
510
+ * @param {Object} params
511
+ * @param {THREE.BufferGeometry} params.geometry - Part geometry
512
+ * @param {number} params.layerHeight - Layer height
513
+ * @param {number} params.nozzleWidth - Nozzle width
514
+ * @param {string} params.infillPattern - 'grid' | 'honeycomb' | 'gyroid'
515
+ * @param {number} params.infillDensity - 0-1 (0.2 = 20%)
516
+ * @returns {Object} Sliced object
517
+ */
518
+ function generateFDMSlicing(params = {}) {
519
+ const {
520
+ geometry = null,
521
+ layerHeight = 0.2,
522
+ nozzleWidth = 0.4,
523
+ infillPattern = 'grid',
524
+ infillDensity = 0.2,
525
+ } = params;
526
+
527
+ const id = `fdm_${toolpathCounter.count++}`;
528
+
529
+ // Compute bounding box
530
+ const bbox = new THREE.Box3().setFromBufferGeometry(geometry);
531
+ const height = bbox.max.z - bbox.min.z;
532
+ const layerCount = Math.ceil(height / layerHeight);
533
+
534
+ // Generate layers
535
+ const layers = [];
536
+ for (let i = 0; i < layerCount; i++) {
537
+ const z = bbox.min.z + i * layerHeight;
538
+ layers.push({
539
+ z,
540
+ index: i,
541
+ perimeter: generatePerimeterPaths(geometry, z),
542
+ infill: generateInfillPattern(geometry, z, infillPattern, infillDensity),
543
+ });
544
+ }
545
+
546
+ const slicing = {
547
+ id,
548
+ type: 'fdm_slicing',
549
+ geometry,
550
+ layerHeight,
551
+ nozzleWidth,
552
+ infillPattern,
553
+ infillDensity,
554
+ layers,
555
+ estimatedTime: layerCount * 2, // ~2min per layer estimate
556
+ filamentLength: estimateFilamentLength(layers),
557
+ filamentWeight: 0, // would need material density
558
+ };
559
+
560
+ camState.toolpaths.set(id, slicing);
561
+ console.log('[CAM] FDM slicing generated:', { id, layers: layerCount });
562
+ window.dispatchEvent(new CustomEvent('cam:toolpathGenerated', { detail: slicing }));
563
+
564
+ return { id, type: 'fdm_slicing', layers: layerCount, estimatedTime: slicing.estimatedTime };
565
+ }
566
+
567
+ /**
568
+ * Generate G-code from toolpath
569
+ * @param {string} toolpathId - Toolpath ID
570
+ * @param {string} dialect - 'fanuc' | 'linuxcnc' | 'grbl' | 'marlin'
571
+ * @returns {string} G-code text
572
+ */
573
+ function generateGCode(toolpathId, dialect = 'grbl') {
574
+ const toolpath = camState.toolpaths.get(toolpathId);
575
+ if (!toolpath) throw new Error(`Toolpath ${toolpathId} not found`);
576
+
577
+ let gcode = '';
578
+
579
+ // Header
580
+ gcode += '; Generated by cycleCAD CAM\n';
581
+ gcode += `; Machine: ${dialect}\n`;
582
+ gcode += `; Generated: ${new Date().toISOString()}\n`;
583
+ gcode += `; Tool: ${toolpath.tool.name}\n`;
584
+ gcode += `; Operation: ${toolpath.type}\n`;
585
+ gcode += ';\n';
586
+
587
+ // Unit setup
588
+ if (dialect === 'grbl' || dialect === 'linuxcnc') {
589
+ gcode += 'G90 G21\n'; // absolute, metric
590
+ } else if (dialect === 'fanuc') {
591
+ gcode += 'G90 G21\n'; // absolute, metric
592
+ }
593
+
594
+ // Spindle on
595
+ gcode += `S${toolpath.tool.rpm} M3\n`;
596
+
597
+ // Generate moves based on toolpath type
598
+ if (toolpath.type === 'drilling') {
599
+ gcode += generateDrillingGCode(toolpath, dialect);
600
+ } else if (toolpath.type === 'contour_2d') {
601
+ gcode += generateContourGCode(toolpath, dialect);
602
+ } else if (toolpath.type === 'pocket' || toolpath.type === 'adaptive_clearing') {
603
+ gcode += generatePocketGCode(toolpath, dialect);
604
+ } else if (toolpath.type === 'face') {
605
+ gcode += generateFaceGCode(toolpath, dialect);
606
+ }
607
+
608
+ // End
609
+ gcode += '\nM5\n'; // Spindle off
610
+ gcode += 'M30\n'; // Program end
611
+
612
+ camState.gcode = gcode;
613
+
614
+ console.log('[CAM] G-code generated:', { length: gcode.length, lines: gcode.split('\n').length - 1 });
615
+ window.dispatchEvent(new CustomEvent('cam:gcodeGenerated', { detail: { gcode, length: gcode.length } }));
616
+
617
+ return gcode;
618
+ }
619
+
620
+ /**
621
+ * Simulate toolpath motion in 3D
622
+ * @param {string} toolpathId - Toolpath ID
623
+ * @param {number} speed - Playback speed (1.0 = real-time, 10 = 10x faster)
624
+ * @returns {Object} Simulation controller
625
+ */
626
+ function simulateToolpath(toolpathId, speed = 1.0) {
627
+ const toolpath = camState.toolpaths.get(toolpathId);
628
+ if (!toolpath) throw new Error(`Toolpath ${toolpathId} not found`);
629
+
630
+ const tool = toolpath.tool;
631
+ const simulation = {
632
+ toolpathId,
633
+ isRunning: false,
634
+ progress: 0,
635
+ startTime: 0,
636
+ totalTime: toolpath.estimatedTime * 1000 / speed,
637
+
638
+ // Create tool mesh
639
+ toolMesh: createToolMesh(tool),
640
+ };
641
+
642
+ if (viewport?.scene) {
643
+ viewport.scene.add(simulation.toolMesh);
644
+ }
645
+
646
+ // Animate tool
647
+ const animate = () => {
648
+ if (!simulation.isRunning) return;
649
+
650
+ const elapsed = Date.now() - simulation.startTime;
651
+ simulation.progress = Math.min(elapsed / simulation.totalTime, 1.0);
652
+
653
+ // Position tool along toolpath
654
+ const pathPoints = extractPathPoints(toolpath);
655
+ if (pathPoints.length > 0) {
656
+ const pointIndex = Math.floor(simulation.progress * pathPoints.length);
657
+ const point = pathPoints[Math.min(pointIndex, pathPoints.length - 1)];
658
+ simulation.toolMesh.position.copy(point);
659
+ }
660
+
661
+ if (simulation.progress < 1.0) {
662
+ requestAnimationFrame(animate);
663
+ } else {
664
+ simulation.isRunning = false;
665
+ window.dispatchEvent(new CustomEvent('cam:simulationComplete', { detail: simulation }));
666
+ }
667
+ };
668
+
669
+ return {
670
+ start: () => {
671
+ simulation.isRunning = true;
672
+ simulation.startTime = Date.now();
673
+ animate();
674
+ },
675
+ stop: () => {
676
+ simulation.isRunning = false;
677
+ },
678
+ pause: () => {
679
+ // In real implementation, would handle pause
680
+ },
681
+ getProgress: () => simulation.progress,
682
+ getMesh: () => simulation.toolMesh,
683
+ };
684
+ }
685
+
686
+ /**
687
+ * Set active tool
688
+ * @param {string} toolId - Tool ID from library
689
+ */
690
+ function setTool(toolId) {
691
+ const tool = camState.toolLibrary.get(toolId);
692
+ if (!tool) throw new Error(`Tool ${toolId} not found`);
693
+ camState.selectedTool = tool;
694
+ console.log('[CAM] Tool selected:', tool.name);
695
+ return tool;
696
+ }
697
+
698
+ /**
699
+ * Add custom tool to library
700
+ * @param {Object} toolDef - Tool definition
701
+ * @returns {Object} Added tool
702
+ */
703
+ function addTool(toolDef) {
704
+ const id = toolDef.id || `custom_tool_${Date.now()}`;
705
+ const tool = { id, ...toolDef };
706
+ camState.toolLibrary.set(id, tool);
707
+ console.log('[CAM] Tool added:', tool.name);
708
+ return tool;
709
+ }
710
+
711
+ /**
712
+ * List all tools in library
713
+ */
714
+ function listTools() {
715
+ return Array.from(camState.toolLibrary.values());
716
+ }
717
+
718
+ /**
719
+ * Export G-code to file
720
+ * @param {string} filename - Filename
721
+ * @param {string} content - G-code content
722
+ */
723
+ function exportGCode(filename, content) {
724
+ const blob = new Blob([content || camState.gcode], { type: 'text/plain' });
725
+ const url = URL.createObjectURL(blob);
726
+ const a = document.createElement('a');
727
+ a.href = url;
728
+ a.download = filename || 'toolpath.nc';
729
+ a.click();
730
+ URL.revokeObjectURL(url);
731
+
732
+ console.log('[CAM] G-code exported:', filename);
733
+ window.dispatchEvent(new CustomEvent('cam:gcodeExported', { detail: { filename } }));
734
+ }
735
+
736
+ /**
737
+ * List all toolpaths
738
+ */
739
+ function listToolpaths() {
740
+ return Array.from(camState.toolpaths.entries()).map(([id, tp]) => ({
741
+ id,
742
+ type: tp.type,
743
+ tool: tp.tool.name,
744
+ estimatedTime: tp.estimatedTime,
745
+ status: tp.status,
746
+ }));
747
+ }
748
+
749
+ // --- Helper Functions ---
750
+
751
+ function createStockGeometry(type, dimensions) {
752
+ if (type === 'box') {
753
+ return new THREE.BoxGeometry(dimensions.x, dimensions.y, dimensions.z);
754
+ } else if (type === 'cylinder') {
755
+ return new THREE.CylinderGeometry(dimensions.diameter / 2, dimensions.diameter / 2, dimensions.height, 32);
756
+ }
757
+ return new THREE.BoxGeometry(100, 100, 50);
758
+ }
759
+
760
+ function offsetProfile(profile, offset) {
761
+ // Simple offset (in real impl, use 2D offset library)
762
+ return profile.map(pt => new THREE.Vector3(pt.x + offset, pt.y, pt.z));
763
+ }
764
+
765
+ function generateSpiralPattern(region, stepOver) {
766
+ // Generate spiral toolpath
767
+ const lines = [];
768
+ for (let r = 0; r < 10; r += stepOver) {
769
+ lines.push({ radius: r, points: [] });
770
+ }
771
+ return lines;
772
+ }
773
+
774
+ function generateRasterPattern(region, stepOver) {
775
+ // Generate back-and-forth raster
776
+ const passes = [];
777
+ for (let x = 0; x < 100; x += stepOver) {
778
+ passes.push({ x, path: [] });
779
+ }
780
+ return passes;
781
+ }
782
+
783
+ function generateAdaptivePattern(region, stepOver, depth) {
784
+ // Adaptive cutting pattern with variable engagement
785
+ return { pattern: 'adaptive', depth };
786
+ }
787
+
788
+ function generateParallelPasses(geometry, stepOver, direction) {
789
+ const passes = [];
790
+ for (let i = 0; i < 50; i += stepOver) {
791
+ passes.push({ offset: i, path: [] });
792
+ }
793
+ return passes;
794
+ }
795
+
796
+ function generatePerimeterPaths(geometry, z) {
797
+ // Generate perimeter toolpath at Z height
798
+ return [];
799
+ }
800
+
801
+ function generateInfillPattern(geometry, z, pattern, density) {
802
+ // Generate infill pattern (grid/honeycomb/gyroid)
803
+ return [];
804
+ }
805
+
806
+ function estimateFilamentLength(layers) {
807
+ // Estimate total filament length for FDM
808
+ return layers.length * 10; // placeholder
809
+ }
810
+
811
+ function calculateEstimatedTime(region, passes, tool) {
812
+ // Very rough time estimate
813
+ const passLength = region.length || 50; // mm
814
+ const speedMMMin = tool.feed || 1000;
815
+ const totalDistance = passes.length * passLength;
816
+ return (totalDistance / speedMMMin) * 60; // seconds
817
+ }
818
+
819
+ function calculateDrillingTime(points, depth, tool) {
820
+ // Drilling time = (tool penetration rate) * depth * number of points
821
+ const penetrationRate = 10; // mm/min
822
+ return (points.length * depth / penetrationRate) * 60; // seconds
823
+ }
824
+
825
+ function extractPathPoints(toolpath) {
826
+ // Extract all motion points from toolpath
827
+ const points = [];
828
+ if (toolpath.passes) {
829
+ toolpath.passes.forEach(pass => {
830
+ if (pass.points) points.push(...pass.points);
831
+ if (pass.path) points.push(...pass.path);
832
+ });
833
+ }
834
+ return points;
835
+ }
836
+
837
+ function createToolMesh(tool) {
838
+ // Create 3D mesh representing the tool
839
+ let geom;
840
+ if (tool.type === 'ball') {
841
+ geom = new THREE.SphereGeometry(tool.diameter / 2, 16, 16);
842
+ } else if (tool.type === 'drill') {
843
+ geom = new THREE.ConeGeometry(tool.diameter / 2, tool.diameter, 8);
844
+ } else {
845
+ geom = new THREE.CylinderGeometry(tool.diameter / 2, tool.diameter / 2, tool.fluteLength, 16);
846
+ }
847
+ const mat = new THREE.MeshPhongMaterial({ color: 0xffaa00 });
848
+ return new THREE.Mesh(geom, mat);
849
+ }
850
+
851
+ function visualizeToolpath(toolpath) {
852
+ // Draw toolpath as lines in viewport
853
+ if (!viewport?.scene) return;
854
+
855
+ const geometry = new THREE.BufferGeometry();
856
+ const points = extractPathPoints(toolpath);
857
+ if (points.length > 0) {
858
+ geometry.setFromPoints(points);
859
+ const line = new THREE.LineSegments(
860
+ geometry,
861
+ new THREE.LineBasicMaterial({ color: 0x00ff00, linewidth: 2 })
862
+ );
863
+ line.name = `toolpath_${toolpath.id}`;
864
+ viewport.scene.add(line);
865
+ }
866
+ }
867
+
868
+ function generateDrillingGCode(toolpath, dialect) {
869
+ let gcode = '';
870
+ toolpath.drillSequence.forEach((drill, i) => {
871
+ gcode += `G0 X${drill.point.x.toFixed(3)} Y${drill.point.y.toFixed(3)}\n`;
872
+ gcode += `G0 Z${camState.setupParams.safeHeight}\n`;
873
+ gcode += `G1 Z${-drill.depth} F${toolpath.tool.feed}\n`;
874
+ if (toolpath.type.includes('peck') && drill.pecks > 1) {
875
+ for (let p = 0; p < drill.pecks; p++) {
876
+ gcode += `G0 Z${camState.setupParams.retractHeight}\n`;
877
+ gcode += `G1 Z${-Math.min((p + 1) * drill.peckDepth, drill.depth)} F${toolpath.tool.feed}\n`;
878
+ }
879
+ }
880
+ gcode += `G0 Z${camState.setupParams.safeHeight}\n`;
881
+ });
882
+ return gcode;
883
+ }
884
+
885
+ function generateContourGCode(toolpath, dialect) {
886
+ let gcode = '';
887
+ toolpath.passes.forEach((pass, i) => {
888
+ gcode += `\n; Pass ${i + 1} - Depth ${pass.depth}\n`;
889
+ pass.points.forEach(pt => {
890
+ gcode += `G0 X${pt.x.toFixed(3)} Y${pt.y.toFixed(3)}\n`;
891
+ gcode += `G1 Z${-pass.depth} F${toolpath.tool.feed}\n`;
892
+ });
893
+ gcode += `G0 Z${camState.setupParams.safeHeight}\n`;
894
+ });
895
+ return gcode;
896
+ }
897
+
898
+ function generatePocketGCode(toolpath, dialect) {
899
+ let gcode = '';
900
+ toolpath.passes.forEach((pass, i) => {
901
+ gcode += `\n; Pass ${i + 1} - Depth ${pass.depth}\n`;
902
+ if (pass.lines) {
903
+ pass.lines.forEach(line => {
904
+ gcode += `G1 X${line.x.toFixed(3)} Y${line.y.toFixed(3)} Z${-pass.depth} F${toolpath.tool.feed}\n`;
905
+ });
906
+ }
907
+ });
908
+ gcode += `G0 Z${camState.setupParams.safeHeight}\n`;
909
+ return gcode;
910
+ }
911
+
912
+ function generateFaceGCode(toolpath, dialect) {
913
+ let gcode = '';
914
+ toolpath.passes.forEach((pass, i) => {
915
+ gcode += `\n; Pass ${i + 1}\n`;
916
+ gcode += `G0 X${pass.x} Y0\n`;
917
+ gcode += `G1 Y100 F${toolpath.tool.feed}\n`;
918
+ gcode += `G0 Y0\n`;
919
+ });
920
+ gcode += `G0 Z${camState.setupParams.safeHeight}\n`;
921
+ return gcode;
922
+ }
923
+
924
+ // --- Command Registration ---
925
+
926
+ function registerCommands() {
927
+ const api = window.cycleCAD?.api || {};
928
+
929
+ api.cam = {
930
+ setup: setupWorkCoordinateSystem,
931
+ contour2d: generateContour2D,
932
+ pocket: generatePocket,
933
+ drill: generateDrilling,
934
+ face: generateFace,
935
+ adaptive: generateAdaptiveClearing,
936
+ parallel: generateParallel,
937
+ slice: generateFDMSlicing,
938
+ generateGCode,
939
+ simulate: simulateToolpath,
940
+ setTool,
941
+ addTool,
942
+ listTools,
943
+ listToolpaths,
944
+ exportGCode,
945
+ getState: () => camState,
946
+ };
947
+
948
+ window.cycleCAD = window.cycleCAD || {};
949
+ window.cycleCAD.api = api;
950
+ }
951
+
952
+ // --- Keyboard Shortcuts ---
953
+
954
+ function handleKeyboard(evt) {
955
+ if (evt.ctrlKey && evt.shiftKey && evt.key === 'M') {
956
+ console.log('[CAM] Active toolpaths:', listToolpaths());
957
+ evt.preventDefault();
958
+ }
959
+ }
960
+
961
+ // --- UI Panel ---
962
+
963
+ function getUI() {
964
+ ui = document.createElement('div');
965
+ ui.id = 'cam-panel';
966
+ ui.className = 'module-panel';
967
+ ui.innerHTML = `
968
+ <div class="panel-header">
969
+ <h3>CAM Setup & Toolpaths</h3>
970
+ <button class="close-btn" data-close-panel="#cam-panel">×</button>
971
+ </div>
972
+ <div class="panel-body" style="max-height: 400px; overflow-y: auto;">
973
+ <fieldset style="margin-bottom: 10px;">
974
+ <legend>Work Setup</legend>
975
+ <label>Stock Type:</label>
976
+ <select id="cam-stock-type" style="margin-bottom: 5px;">
977
+ <option value="box">Box</option>
978
+ <option value="cylinder">Cylinder</option>
979
+ </select>
980
+ <button class="module-btn" data-cmd="cam.setup" style="width: 100%;">Define WCS</button>
981
+ </fieldset>
982
+
983
+ <fieldset style="margin-bottom: 10px;">
984
+ <legend>2D Milling</legend>
985
+ <div class="button-group" style="display: grid; grid-template-columns: 1fr 1fr; gap: 5px;">
986
+ <button class="module-btn" data-cmd="cam.contour2d">Contour 2D</button>
987
+ <button class="module-btn" data-cmd="cam.pocket">Pocket</button>
988
+ <button class="module-btn" data-cmd="cam.drill">Drill</button>
989
+ <button class="module-btn" data-cmd="cam.face">Face</button>
990
+ </div>
991
+ </fieldset>
992
+
993
+ <fieldset style="margin-bottom: 10px;">
994
+ <legend>3D Milling</legend>
995
+ <div class="button-group" style="display: grid; grid-template-columns: 1fr 1fr; gap: 5px;">
996
+ <button class="module-btn" data-cmd="cam.adaptive">Adaptive</button>
997
+ <button class="module-btn" data-cmd="cam.parallel">Parallel</button>
998
+ </div>
999
+ </fieldset>
1000
+
1001
+ <fieldset style="margin-bottom: 10px;">
1002
+ <legend>Additive</legend>
1003
+ <button class="module-btn" data-cmd="cam.slice" style="width: 100%;">FDM Slice</button>
1004
+ </fieldset>
1005
+
1006
+ <fieldset style="margin-bottom: 10px;">
1007
+ <legend>Tool Library</legend>
1008
+ <select id="cam-tool-select" style="width: 100%; margin-bottom: 5px;">
1009
+ ${Array.from(defaultToolLibrary.values()).map(t => `<option value="${t.id}">${t.name}</option>`).join('')}
1010
+ </select>
1011
+ <button class="module-btn" data-cmd="cam.setTool" style="width: 100%;">Select Tool</button>
1012
+ </fieldset>
1013
+
1014
+ <fieldset style="margin-bottom: 10px;">
1015
+ <legend>Output</legend>
1016
+ <label>G-code Dialect:</label>
1017
+ <select id="cam-dialect" style="width: 100%; margin-bottom: 5px;">
1018
+ <option value="grbl">Grbl</option>
1019
+ <option value="linuxcnc">LinuxCNC</option>
1020
+ <option value="fanuc">Fanuc</option>
1021
+ <option value="marlin">Marlin (3D Printer)</option>
1022
+ </select>
1023
+ <button class="module-btn" data-cmd="cam.generateGCode" style="width: 100%; margin-bottom: 5px;">Generate G-code</button>
1024
+ <button class="module-btn" data-cmd="cam.exportGCode" style="width: 100%;">Export G-code</button>
1025
+ </fieldset>
1026
+
1027
+ <div id="cam-toolpath-list" style="padding: 10px; border: 1px solid #ccc; border-radius: 4px; background: #f5f5f5;">
1028
+ <strong>Toolpaths:</strong>
1029
+ <ul id="cam-toolpath-items" style="list-style: none; padding: 0; margin: 5px 0; font-size: 12px;"></ul>
1030
+ </div>
1031
+ </div>
1032
+ `;
1033
+
1034
+ // Wire up buttons
1035
+ ui.querySelectorAll('[data-cmd]').forEach(btn => {
1036
+ btn.addEventListener('click', () => {
1037
+ const [ns, cmd] = btn.dataset.cmd.split('.');
1038
+ console.log(`[CAM] Command: ${cmd}`);
1039
+ });
1040
+ });
1041
+
1042
+ return ui;
1043
+ }
1044
+
1045
+ return {
1046
+ MODULE_NAME,
1047
+ init,
1048
+ getUI,
1049
+ setupWorkCoordinateSystem,
1050
+ generateContour2D,
1051
+ generatePocket,
1052
+ generateDrilling,
1053
+ generateFace,
1054
+ generateAdaptiveClearing,
1055
+ generateParallel,
1056
+ generateFDMSlicing,
1057
+ generateGCode,
1058
+ simulateToolpath,
1059
+ setTool,
1060
+ addTool,
1061
+ listTools,
1062
+ listToolpaths,
1063
+ exportGCode,
1064
+ };
1065
+ })();
1066
+
1067
+ export default CAMModule;