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.
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/.github/scripts/cad-diff.js +0 -590
- 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;
|