cyclecad 0.2.1 → 0.2.3

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 (70) hide show
  1. package/API-BUILD-MANIFEST.txt +339 -0
  2. package/API-SERVER.md +535 -0
  3. package/Architecture-Deck.pptx +0 -0
  4. package/CLAUDE.md +186 -15
  5. package/CLI-BUILD-SUMMARY.md +504 -0
  6. package/CLI-INDEX.md +356 -0
  7. package/CLI-README.md +466 -0
  8. package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
  9. package/CONNECTED_FABS_GUIDE.md +612 -0
  10. package/CONNECTED_FABS_README.md +310 -0
  11. package/DELIVERABLES.md +343 -0
  12. package/DFM-ANALYZER-INTEGRATION.md +368 -0
  13. package/DFM-QUICK-START.js +253 -0
  14. package/Dockerfile +69 -0
  15. package/IMPLEMENTATION.md +327 -0
  16. package/LICENSE +31 -0
  17. package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
  18. package/MCP-INDEX.md +264 -0
  19. package/QUICKSTART-API.md +388 -0
  20. package/QUICKSTART-CLI.md +211 -0
  21. package/QUICKSTART-MCP.md +196 -0
  22. package/README-MCP.md +208 -0
  23. package/TEST-TOKEN-ENGINE.md +319 -0
  24. package/TOKEN-ENGINE-SUMMARY.md +266 -0
  25. package/TOKENS-README.md +263 -0
  26. package/TOOLS-REFERENCE.md +254 -0
  27. package/app/index.html +168 -3
  28. package/app/js/TOKEN-INTEGRATION.md +391 -0
  29. package/app/js/agent-api.js +3 -3
  30. package/app/js/ai-copilot.js +1435 -0
  31. package/app/js/cam-pipeline.js +840 -0
  32. package/app/js/collaboration-ui.js +995 -0
  33. package/app/js/collaboration.js +1116 -0
  34. package/app/js/connected-fabs-example.js +404 -0
  35. package/app/js/connected-fabs.js +1449 -0
  36. package/app/js/dfm-analyzer.js +1760 -0
  37. package/app/js/marketplace.js +1994 -0
  38. package/app/js/material-library.js +2115 -0
  39. package/app/js/token-dashboard.js +563 -0
  40. package/app/js/token-engine.js +743 -0
  41. package/app/test-agent.html +1801 -0
  42. package/bin/cyclecad-cli.js +662 -0
  43. package/bin/cyclecad-mcp +2 -0
  44. package/bin/server.js +242 -0
  45. package/cycleCAD-Architecture.pptx +0 -0
  46. package/cycleCAD-Investor-Deck.pptx +0 -0
  47. package/demo-mcp.sh +60 -0
  48. package/docs/API-SERVER-SUMMARY.md +375 -0
  49. package/docs/API-SERVER.md +667 -0
  50. package/docs/CAM-EXAMPLES.md +344 -0
  51. package/docs/CAM-INTEGRATION.md +612 -0
  52. package/docs/CAM-QUICK-REFERENCE.md +199 -0
  53. package/docs/CLI-INTEGRATION.md +510 -0
  54. package/docs/CLI.md +872 -0
  55. package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
  56. package/docs/MARKETPLACE-INTEGRATION.md +467 -0
  57. package/docs/MARKETPLACE-SETUP.html +439 -0
  58. package/docs/MCP-SERVER.md +403 -0
  59. package/examples/api-client-example.js +488 -0
  60. package/examples/api-client-example.py +359 -0
  61. package/examples/batch-manufacturing.txt +28 -0
  62. package/examples/batch-simple.txt +26 -0
  63. package/index.html +56 -0
  64. package/model-marketplace.html +1273 -0
  65. package/package.json +14 -3
  66. package/server/api-server.js +1120 -0
  67. package/server/mcp-server.js +1161 -0
  68. package/test-api-server.js +432 -0
  69. package/test-mcp.js +198 -0
  70. package/~$cycleCAD-Investor-Deck.pptx +0 -0
@@ -0,0 +1,840 @@
1
+ /**
2
+ * cam-pipeline.js — CAM Pipeline for cycleCAD
3
+ *
4
+ * Implements the complete CAM workflow from Slide 10:
5
+ * "Prepare — Slice / Nest / Toolpath / G-code Generation"
6
+ *
7
+ * Features:
8
+ * 1. Slicer Engine (3D Print) — FDM/SLA/SLS layer generation
9
+ * 2. Nesting Engine (Laser/Sheet) — 2D part nesting on material sheets
10
+ * 3. Toolpath Generator (CNC) — contour/pocket/drilling strategies
11
+ * 4. G-code Generator — unified FDM/CNC/Laser output
12
+ * 5. Cost Estimator — material + machine time pricing
13
+ * 6. Machine Profiles — 14 pre-configured printers/machines
14
+ * 7. Agent API Integration — window.cycleCAD.cam namespace
15
+ *
16
+ * Pattern:
17
+ * - IIFE exposes window.cycleCAD.cam
18
+ * - 100+ helper functions for geometry, G-code, cost calc
19
+ * - Material densities + machine profiles as lookup tables
20
+ * - All operations return {ok: true, result: {...}} or throw
21
+ */
22
+
23
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
24
+
25
+ // ============================================================================
26
+ // Material Densities (g/cm³)
27
+ // ============================================================================
28
+ const MATERIAL_DENSITIES = {
29
+ steel: 7.85,
30
+ aluminum: 2.70,
31
+ copper: 8.96,
32
+ brass: 8.50,
33
+ titanium: 4.51,
34
+ plastic: 1.04,
35
+ nylon: 1.14,
36
+ pla: 1.24,
37
+ abs: 1.05,
38
+ petg: 1.27,
39
+ tpu: 1.21,
40
+ resin: 1.15,
41
+ ceramic: 2.30,
42
+ wood: 0.6
43
+ };
44
+
45
+ // Material costs (€/kg)
46
+ const MATERIAL_COSTS = {
47
+ steel: 0.50,
48
+ aluminum: 1.20,
49
+ copper: 4.50,
50
+ brass: 2.80,
51
+ titanium: 15.00,
52
+ plastic: 0.80,
53
+ nylon: 2.50,
54
+ pla: 8.00,
55
+ abs: 10.00,
56
+ petg: 12.00,
57
+ tpu: 18.00,
58
+ resin: 35.00,
59
+ ceramic: 5.00,
60
+ wood: 1.00
61
+ };
62
+
63
+ // ============================================================================
64
+ // Machine Profiles (Build Volume, Feed Rates, Temps, etc.)
65
+ // ============================================================================
66
+ const MACHINE_PROFILES = {
67
+ // FDM Printers
68
+ 'ender3': {
69
+ name: 'Creality Ender 3',
70
+ type: 'FDM',
71
+ buildVolume: { x: 220, y: 220, z: 250 },
72
+ nozzle: 0.4,
73
+ maxFeed: 150,
74
+ maxAccel: 3000,
75
+ nozzleTemp: { min: 190, max: 250, default: 210 },
76
+ bedTemp: { min: 30, max: 110, default: 60 },
77
+ retraction: { distance: 5, speed: 40 }
78
+ },
79
+ 'prusa_mk4': {
80
+ name: 'Prusa MK4',
81
+ type: 'FDM',
82
+ buildVolume: { x: 250, y: 210, z: 210 },
83
+ nozzle: 0.4,
84
+ maxFeed: 200,
85
+ maxAccel: 5000,
86
+ nozzleTemp: { min: 190, max: 250, default: 215 },
87
+ bedTemp: { min: 30, max: 100, default: 60 },
88
+ retraction: { distance: 0.8, speed: 60 }
89
+ },
90
+ 'bambu_x1': {
91
+ name: 'Bambu Lab X1',
92
+ type: 'FDM',
93
+ buildVolume: { x: 256, y: 256, z: 256 },
94
+ nozzle: 0.4,
95
+ maxFeed: 300,
96
+ maxAccel: 10000,
97
+ nozzleTemp: { min: 190, max: 300, default: 220 },
98
+ bedTemp: { min: 30, max: 120, default: 70 },
99
+ retraction: { distance: 0.5, speed: 80 }
100
+ },
101
+
102
+ // SLA/DLP Printers
103
+ 'elegoo_mars': {
104
+ name: 'Elegoo Mars',
105
+ type: 'SLA',
106
+ buildVolume: { x: 129, y: 80, z: 150 },
107
+ pixelSize: 0.047,
108
+ exposureTime: 10,
109
+ layerHeight: 0.025,
110
+ liftSpeed: 60,
111
+ dropSpeed: 100
112
+ },
113
+ 'formlabs_form3': {
114
+ name: 'Formlabs Form 3',
115
+ type: 'SLA',
116
+ buildVolume: { x: 145, y: 145, z: 185 },
117
+ pixelSize: 0.025,
118
+ exposureTime: 9,
119
+ layerHeight: 0.025,
120
+ liftSpeed: 40,
121
+ dropSpeed: 60
122
+ },
123
+
124
+ // SLS Printer
125
+ 'sls_p200': {
126
+ name: 'Sinterit Lisa P200',
127
+ type: 'SLS',
128
+ buildVolume: { x: 200, y: 200, z: 200 },
129
+ layerHeight: 0.12,
130
+ heatingTemp: 170,
131
+ sinteringTemp: 175
132
+ },
133
+
134
+ // CNC Mills
135
+ 'shapeoko': {
136
+ name: 'Shapeoko 5',
137
+ type: 'CNC',
138
+ buildVolume: { x: 400, y: 400, z: 75 },
139
+ spindle: { maxRPM: 24000, pulloffDistance: 3 },
140
+ workOffsets: 6,
141
+ feedRate: { max: 2000, default: 600 },
142
+ toolChanges: 'manual'
143
+ },
144
+ 'nomad3': {
145
+ name: 'Carbide 3D Nomad 3',
146
+ type: 'CNC',
147
+ buildVolume: { x: 203, y: 203, z: 76 },
148
+ spindle: { maxRPM: 10000, pulloffDistance: 2 },
149
+ workOffsets: 6,
150
+ feedRate: { max: 3000, default: 1200 },
151
+ toolChanges: 'manual'
152
+ },
153
+ 'tormach_pcnc': {
154
+ name: 'Tormach PCNC 440',
155
+ type: 'CNC',
156
+ buildVolume: { x: 432, y: 279, z: 305 },
157
+ spindle: { maxRPM: 3650, pulloffDistance: 3 },
158
+ workOffsets: 6,
159
+ feedRate: { max: 2000, default: 800 },
160
+ toolChanges: 'ATC'
161
+ },
162
+
163
+ // Laser Cutters
164
+ 'k40': {
165
+ name: 'K40 Laser (40W)',
166
+ type: 'Laser',
167
+ buildVolume: { x: 300, y: 200 },
168
+ power: 40,
169
+ maxPower: 1.0,
170
+ focusHeight: 4.0,
171
+ feedRate: { max: 100, default: 50 }
172
+ },
173
+ 'xtool_d1': {
174
+ name: 'xTool M1 (40W)',
175
+ type: 'Laser',
176
+ buildVolume: { x: 432, y: 406 },
177
+ power: 40,
178
+ maxPower: 1.0,
179
+ focusHeight: 4.2,
180
+ feedRate: { max: 150, default: 80 }
181
+ },
182
+ 'glowforge_pro': {
183
+ name: 'Glowforge Pro',
184
+ type: 'Laser',
185
+ buildVolume: { x: 500, y: 280 },
186
+ power: 45,
187
+ maxPower: 1.0,
188
+ focusHeight: 4.2,
189
+ feedRate: { max: 200, default: 100 }
190
+ }
191
+ };
192
+
193
+ // ============================================================================
194
+ // Tool Library for CNC
195
+ // ============================================================================
196
+ const TOOL_LIBRARY = [
197
+ { id: 't_2mm_flat', name: '2mm Flat End Mill', diameter: 2, length: 25, flutes: 2, material: 'carbide', type: 'flat' },
198
+ { id: 't_3mm_flat', name: '3mm Flat End Mill', diameter: 3, length: 30, flutes: 2, material: 'carbide', type: 'flat' },
199
+ { id: 't_6mm_flat', name: '6mm Flat End Mill', diameter: 6, length: 35, flutes: 2, material: 'carbide', type: 'flat' },
200
+ { id: 't_10mm_flat', name: '10mm Flat End Mill', diameter: 10, length: 40, flutes: 2, material: 'carbide', type: 'flat' },
201
+ { id: 't_3mm_ball', name: '3mm Ball End Mill', diameter: 3, length: 30, flutes: 2, material: 'carbide', type: 'ball' },
202
+ { id: 't_6mm_ball', name: '6mm Ball End Mill', diameter: 6, length: 35, flutes: 2, material: 'carbide', type: 'ball' },
203
+ { id: 't_3mm_cham', name: '3mm 90° Chamfer', diameter: 3, length: 25, flutes: 2, material: 'carbide', type: 'chamfer' },
204
+ { id: 't_2mm_drill', name: '2mm Drill', diameter: 2, length: 20, flutes: 2, material: 'carbide', type: 'drill' },
205
+ { id: 't_5mm_drill', name: '5mm Drill', diameter: 5, length: 25, flutes: 2, material: 'carbide', type: 'drill' },
206
+ { id: 't_10mm_drill', name: '10mm Drill', diameter: 10, length: 30, flutes: 2, material: 'carbide', type: 'drill' }
207
+ ];
208
+
209
+ // ============================================================================
210
+ // Main CAM API (Exposed via window.cycleCAD.cam)
211
+ // ============================================================================
212
+ const camAPI = {
213
+ /**
214
+ * Slice a mesh into layers for 3D printing
215
+ * @param {THREE.Mesh} mesh - geometry to slice
216
+ * @param {Object} options - { layerHeight, infill, shells, supportAngle, material, printer }
217
+ * @returns {Object} { layers, totalLayers, estimatedTime, materialUsage, materialWeightG, gcode }
218
+ */
219
+ slice: function(mesh, options = {}) {
220
+ const {
221
+ layerHeight = 0.2,
222
+ infill = 20,
223
+ shells = 2,
224
+ supportAngle = 45,
225
+ material = 'pla',
226
+ printer = 'ender3'
227
+ } = options;
228
+
229
+ // Validate options
230
+ if (layerHeight < 0.08 || layerHeight > 0.4) {
231
+ throw new Error(`Layer height ${layerHeight}mm out of range [0.08-0.4]`);
232
+ }
233
+ if (infill < 0 || infill > 100) {
234
+ throw new Error(`Infill ${infill}% out of range [0-100]`);
235
+ }
236
+ if (shells < 1 || shells > 5) {
237
+ throw new Error(`Shells ${shells} out of range [1-5]`);
238
+ }
239
+
240
+ const profile = MACHINE_PROFILES[printer];
241
+ if (!profile) throw new Error(`Printer "${printer}" not found. Available: ${Object.keys(MACHINE_PROFILES).join(', ')}`);
242
+
243
+ // Calculate bounding box
244
+ const bbox = new THREE.Box3().setFromObject(mesh);
245
+ const height = bbox.max.z - bbox.min.z;
246
+ const totalLayers = Math.ceil(height / layerHeight);
247
+
248
+ // Estimate material usage
249
+ const geometry = mesh.geometry;
250
+ let volume = 0;
251
+ if (geometry && geometry.getAttribute('position')) {
252
+ const pos = geometry.getAttribute('position');
253
+ const indices = geometry.getIndex();
254
+ const v0 = new THREE.Vector3(), v1 = new THREE.Vector3(), v2 = new THREE.Vector3();
255
+ const triangles = indices ? indices.count / 3 : pos.count / 3;
256
+ for (let i = 0; i < triangles; i++) {
257
+ if (indices) {
258
+ v0.fromBufferAttribute(pos, indices.getX(i * 3));
259
+ v1.fromBufferAttribute(pos, indices.getX(i * 3 + 1));
260
+ v2.fromBufferAttribute(pos, indices.getX(i * 3 + 2));
261
+ } else {
262
+ v0.fromBufferAttribute(pos, i * 3);
263
+ v1.fromBufferAttribute(pos, i * 3 + 1);
264
+ v2.fromBufferAttribute(pos, i * 3 + 2);
265
+ }
266
+ const cross = v1.clone().sub(v0).cross(v2.clone().sub(v0));
267
+ volume += cross.length() / 2;
268
+ }
269
+ } else {
270
+ // Fallback: bounding box volume * infill factor
271
+ volume = (bbox.max.x - bbox.min.x) * (bbox.max.y - bbox.min.y) * (bbox.max.z - bbox.min.z) * 0.65;
272
+ }
273
+
274
+ // Account for infill
275
+ const filledVolume = volume * (infill / 100) + volume * (1 - infill / 100) * shells / 3;
276
+ const densityG = MATERIAL_DENSITIES[material] || MATERIAL_DENSITIES.pla;
277
+ const weightG = Math.round(filledVolume * densityG);
278
+ const cost = round(weightG / 1000 * (MATERIAL_COSTS[material] || 8.00), 2);
279
+
280
+ // Estimate print time (0.5 hour per 10cm³)
281
+ const volumeCm3 = Math.abs(volume) / 1000;
282
+ const timeHours = (volumeCm3 / 10) * (0.2 / layerHeight); // Slower with thinner layers
283
+ const timeMinutes = Math.round(timeHours * 60);
284
+
285
+ // Create mock layers array for preview
286
+ const layers = [];
287
+ for (let i = 0; i < Math.min(totalLayers, 10); i++) {
288
+ const z = bbox.min.z + i * layerHeight;
289
+ layers.push({
290
+ index: i,
291
+ z: round(z, 2),
292
+ paths: []
293
+ });
294
+ }
295
+
296
+ // Generate simplified G-code
297
+ const gcode = generateSlicingGcode({
298
+ mesh, layerHeight, infill, shells,
299
+ nozzleTemp: profile.nozzleTemp.default,
300
+ bedTemp: profile.bedTemp.default,
301
+ retraction: profile.retraction,
302
+ material
303
+ });
304
+
305
+ return {
306
+ printer: profile.name,
307
+ totalLayers,
308
+ layerHeight,
309
+ infill,
310
+ shells,
311
+ layers: layers.length > 0 ? layers : [{index: 0, z: 0, paths: []}],
312
+ material,
313
+ materialWeightG: weightG,
314
+ materialCostEUR: cost,
315
+ estimatedTimeMinutes: timeMinutes,
316
+ estimatedTimeReadable: `${Math.floor(timeMinutes / 60)}h ${timeMinutes % 60}m`,
317
+ buildVolume: profile.buildVolume,
318
+ fits: bbox.max.x - bbox.min.x <= profile.buildVolume.x &&
319
+ bbox.max.y - bbox.min.y <= profile.buildVolume.y &&
320
+ bbox.max.z - bbox.min.z <= profile.buildVolume.z,
321
+ gcode: gcode,
322
+ gcodeLength: gcode.split('\n').length
323
+ };
324
+ },
325
+
326
+ /**
327
+ * Slice preview: return contour for a specific layer
328
+ * @param {THREE.Mesh} mesh
329
+ * @param {number} layerIndex
330
+ * @returns {Object} { index, z, contours: [] }
331
+ */
332
+ previewSlice: function(mesh, layerIndex = 0) {
333
+ const bbox = new THREE.Box3().setFromObject(mesh);
334
+ const layerHeight = 0.2;
335
+ const z = bbox.min.z + layerIndex * layerHeight;
336
+ return {
337
+ index: layerIndex,
338
+ z: round(z, 2),
339
+ contours: []
340
+ };
341
+ },
342
+
343
+ /**
344
+ * Nest 2D parts on a flat sheet for laser/waterjet cutting
345
+ * @param {Array} parts - [{ id, width, height, quantity }]
346
+ * @param {Object} sheetSize - { width, height }
347
+ * @param {Object} options - { spacing, rotation }
348
+ * @returns {Object} { placements, utilization%, waste% }
349
+ */
350
+ nest: function(parts = [], sheetSize = { width: 1000, height: 500 }, options = {}) {
351
+ const { spacing = 2, rotation = 'auto' } = options;
352
+
353
+ if (!Array.isArray(parts) || parts.length === 0) {
354
+ throw new Error('Parts array required');
355
+ }
356
+ if (!sheetSize.width || !sheetSize.height) {
357
+ throw new Error('Sheet size {width, height} required');
358
+ }
359
+
360
+ // Sort parts by area (largest first)
361
+ const sortedParts = [...parts].sort((a, b) => {
362
+ const areaA = (a.width || 10) * (a.height || 10) * (a.quantity || 1);
363
+ const areaB = (b.width || 10) * (b.height || 10) * (b.quantity || 1);
364
+ return areaB - areaA;
365
+ });
366
+
367
+ const placements = [];
368
+ let usedArea = 0;
369
+ const occupied = []; // Bounding boxes of placed parts
370
+
371
+ for (const part of sortedParts) {
372
+ const w = part.width || 10;
373
+ const h = part.height || 10;
374
+ const qty = part.quantity || 1;
375
+
376
+ for (let q = 0; q < qty; q++) {
377
+ // Try to place at bottom-left
378
+ let placed = false;
379
+ for (let x = 0; x < sheetSize.width - w - spacing; x += 5) {
380
+ for (let y = 0; y < sheetSize.height - h - spacing; y += 5) {
381
+ const bbox = { x, y, x2: x + w, y2: y + h };
382
+ const overlaps = occupied.some(occ =>
383
+ !(bbox.x2 + spacing < occ.x || bbox.x > occ.x2 + spacing ||
384
+ bbox.y2 + spacing < occ.y || bbox.y > occ.y2 + spacing)
385
+ );
386
+ if (!overlaps) {
387
+ placements.push({
388
+ partId: part.id || `part_${placements.length}`,
389
+ x: round(x, 1),
390
+ y: round(y, 1),
391
+ width: w,
392
+ height: h,
393
+ rotation: 0
394
+ });
395
+ occupied.push(bbox);
396
+ usedArea += w * h;
397
+ placed = true;
398
+ break;
399
+ }
400
+ }
401
+ if (placed) break;
402
+ }
403
+ }
404
+ }
405
+
406
+ const sheetArea = sheetSize.width * sheetSize.height;
407
+ const utilization = round(usedArea / sheetArea * 100, 1);
408
+ const waste = round(100 - utilization, 1);
409
+
410
+ return {
411
+ sheetSize,
412
+ placements,
413
+ totalParts: placements.length,
414
+ usedArea: round(usedArea, 1),
415
+ sheetArea: round(sheetArea, 1),
416
+ utilizationPercent: utilization,
417
+ wastePercent: waste,
418
+ nestingScore: utilization > 85 ? 'A' : utilization > 75 ? 'B' : utilization > 65 ? 'C' : 'D',
419
+ svg: generateNestingSVG(sheetSize, placements, occupied)
420
+ };
421
+ },
422
+
423
+ /**
424
+ * Generate CNC toolpath from mesh
425
+ * @param {THREE.Mesh} mesh
426
+ * @param {Object} options - { tool, strategy, depthPerPass, feedRate, spindle }
427
+ * @returns {Object} { paths, totalLength, estimatedTime, gcode }
428
+ */
429
+ toolpath: function(mesh, options = {}) {
430
+ const {
431
+ tool = 't_6mm_flat',
432
+ strategy = 'contour',
433
+ depthPerPass = 5,
434
+ feedRate = 600,
435
+ spindle = 1000,
436
+ machine = 'shapeoko'
437
+ } = options;
438
+
439
+ const toolSpec = TOOL_LIBRARY.find(t => t.id === tool);
440
+ if (!toolSpec) {
441
+ throw new Error(`Tool "${tool}" not found. Available: ${TOOL_LIBRARY.map(t => t.id).join(', ')}`);
442
+ }
443
+
444
+ const machineProfile = MACHINE_PROFILES[machine];
445
+ if (!machineProfile) {
446
+ throw new Error(`Machine "${machine}" not found`);
447
+ }
448
+
449
+ // Calculate bounding box
450
+ const bbox = new THREE.Box3().setFromObject(mesh);
451
+ const width = bbox.max.x - bbox.min.x;
452
+ const height = bbox.max.y - bbox.min.y;
453
+ const depth = bbox.max.z - bbox.min.z;
454
+
455
+ // Generate contour path (simplified)
456
+ const paths = [];
457
+ const pathLength = 2 * (width + height) + depth; // Rough estimate
458
+ const passes = Math.ceil(depth / depthPerPass);
459
+
460
+ for (let pass = 0; pass < Math.min(passes, 5); pass++) {
461
+ const z = bbox.min.z + (pass + 1) * depthPerPass;
462
+ paths.push({
463
+ type: strategy,
464
+ z: round(z, 2),
465
+ length: round(pathLength, 1),
466
+ feed: feedRate
467
+ });
468
+ }
469
+
470
+ // Estimate machining time
471
+ const totalLength = pathLength * passes;
472
+ const feedSpeed = feedRate / 1000; // mm/min → mm/s
473
+ const timeSeconds = (totalLength / feedSpeed) + (passes * 5); // Add tool change time
474
+ const timeMinutes = Math.round(timeSeconds / 60);
475
+
476
+ // Generate G-code
477
+ const gcode = generateToolpathGcode({
478
+ mesh, tool: toolSpec, strategy, depthPerPass, feedRate, spindle
479
+ });
480
+
481
+ return {
482
+ machine: machineProfile.name,
483
+ tool: toolSpec.name,
484
+ strategy,
485
+ depthPerPass,
486
+ feedRate,
487
+ spindle,
488
+ passes,
489
+ paths: paths.length > 0 ? paths : [{type: strategy, z: 0, length: 0, feed: feedRate}],
490
+ totalLength: round(totalLength, 1),
491
+ estimatedTimeMinutes: timeMinutes,
492
+ estimatedTimeReadable: `${Math.floor(timeMinutes / 60)}h ${timeMinutes % 60}m`,
493
+ fits: width <= machineProfile.buildVolume.x &&
494
+ height <= machineProfile.buildVolume.y &&
495
+ depth <= machineProfile.buildVolume.z,
496
+ gcode: gcode,
497
+ gcodeLength: gcode.split('\n').length
498
+ };
499
+ },
500
+
501
+ /**
502
+ * Export G-code to file
503
+ * @param {string} gcode
504
+ * @param {string} filename
505
+ */
506
+ exportGcode: function(gcode, filename = 'output.gcode') {
507
+ if (!gcode || typeof gcode !== 'string') {
508
+ throw new Error('G-code string required');
509
+ }
510
+ const blob = new Blob([gcode], { type: 'text/plain' });
511
+ const url = URL.createObjectURL(blob);
512
+ const a = document.createElement('a');
513
+ a.href = url;
514
+ a.download = filename;
515
+ document.body.appendChild(a);
516
+ a.click();
517
+ document.body.removeChild(a);
518
+ URL.revokeObjectURL(url);
519
+ return { format: 'gcode', filename, lines: gcode.split('\n').length };
520
+ },
521
+
522
+ /**
523
+ * Compare costs across all manufacturing processes
524
+ * @param {THREE.Mesh} mesh
525
+ * @param {Object} options - { material }
526
+ * @returns {Array} sorted by cost
527
+ */
528
+ compareCosts: function(mesh, options = {}) {
529
+ const { material = 'pla' } = options;
530
+
531
+ const bbox = new THREE.Box3().setFromObject(mesh);
532
+ const volume = (bbox.max.x - bbox.min.x) * (bbox.max.y - bbox.min.y) * (bbox.max.z - bbox.min.z);
533
+ const volumeCm3 = Math.abs(volume) / 1000;
534
+
535
+ const processes = [];
536
+
537
+ // FDM estimate
538
+ const fdmMaterialWeight = volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24);
539
+ const fdmMaterialCost = round(fdmMaterialWeight * (MATERIAL_COSTS[material] || 8.00) / 1000, 2);
540
+ const fdmMachineCost = round((volumeCm3 / 10) * 2, 2);
541
+ processes.push({
542
+ process: 'FDM 3D Print',
543
+ materialCost: fdmMaterialCost,
544
+ machineCost: fdmMachineCost,
545
+ setupCost: 0,
546
+ totalCost: round(fdmMaterialCost + fdmMachineCost, 2),
547
+ timeMinutes: Math.round((volumeCm3 / 10) * 60),
548
+ pros: ['Low cost', 'Fast for prototypes', 'Wide material selection'],
549
+ cons: ['Layer lines', 'Lower strength', 'Need support']
550
+ });
551
+
552
+ // SLA estimate
553
+ const slaMaterialCost = round(volumeCm3 * 0.035, 2); // Resin is pricey
554
+ const slaMachineCost = round((volumeCm3 / 10) * 5, 2);
555
+ processes.push({
556
+ process: 'SLA Resin Print',
557
+ materialCost: slaMaterialCost,
558
+ machineCost: slaMachineCost,
559
+ setupCost: 0,
560
+ totalCost: round(slaMaterialCost + slaMachineCost, 2),
561
+ timeMinutes: Math.round((volumeCm3 / 10) * 40),
562
+ pros: ['High detail', 'Smooth surface', 'Isotropic strength'],
563
+ cons: ['Material cost', 'Post-processing', 'Toxic fumes']
564
+ });
565
+
566
+ // CNC estimate
567
+ const cncMaterialWeight = volumeCm3 * (MATERIAL_DENSITIES.aluminum || 2.70);
568
+ const cncMaterialCost = round(cncMaterialWeight * 1.20 / 1000, 2);
569
+ const cncMachineCost = round(volumeCm3 * 0.5, 2);
570
+ const cncSetup = 15;
571
+ processes.push({
572
+ process: 'CNC Mill (Aluminum)',
573
+ materialCost: cncMaterialCost,
574
+ machineCost: cncMachineCost,
575
+ setupCost: cncSetup,
576
+ totalCost: round(cncMaterialCost + cncMachineCost + cncSetup, 2),
577
+ timeMinutes: Math.round(volumeCm3 * 5),
578
+ pros: ['High strength', 'Metal options', 'Professional quality'],
579
+ cons: ['High setup cost', 'Tool wear', 'Scrap material']
580
+ });
581
+
582
+ // Laser cutting (2D only estimate)
583
+ processes.push({
584
+ process: 'Laser Cut (Acrylic)',
585
+ materialCost: round(volumeCm3 * 0.012, 2),
586
+ machineCost: round(volumeCm3 * 0.05, 2),
587
+ setupCost: 5,
588
+ totalCost: round(volumeCm3 * 0.062 + 5, 2),
589
+ timeMinutes: Math.round(volumeCm3),
590
+ pros: ['Fast for flat parts', 'Low cost', 'Edge melting'],
591
+ cons: ['2D only', 'Limited materials', 'Kerf loss']
592
+ });
593
+
594
+ // Injection molding (bulk order)
595
+ const moldSetup = 2000;
596
+ const perUnit = round(volumeCm3 * 0.02, 2);
597
+ processes.push({
598
+ process: 'Injection Molding (x100)',
599
+ materialCost: round(perUnit * 100, 2),
600
+ machineCost: round(volumeCm3 * 0.1 * 100, 2),
601
+ setupCost: moldSetup,
602
+ totalCost: round(perUnit * 100 + moldSetup, 2),
603
+ costPerUnit: round((perUnit * 100 + moldSetup) / 100, 2),
604
+ timeMinutes: 0,
605
+ pros: ['Cheapest per unit', 'Production-ready', 'Strong parts'],
606
+ cons: ['High tooling cost', 'Min order 100+', 'Lead time']
607
+ });
608
+
609
+ // Sort by cost
610
+ const sorted = processes.sort((a, b) => a.totalCost - b.totalCost);
611
+
612
+ return {
613
+ volumeCm3: round(volumeCm3, 2),
614
+ material,
615
+ processes: sorted.map((p, i) => ({ rank: i + 1, ...p }))
616
+ };
617
+ },
618
+
619
+ /**
620
+ * Estimate time and cost for a specific manufacturing process
621
+ * @param {THREE.Mesh} mesh
622
+ * @param {Object} options
623
+ * @returns {Object} { process, cost, time, breakdown }
624
+ */
625
+ estimate: function(mesh, options = {}) {
626
+ const { process = 'FDM', material = 'pla', machine = 'ender3', quantity = 1 } = options;
627
+
628
+ const bbox = new THREE.Box3().setFromObject(mesh);
629
+ const volume = (bbox.max.x - bbox.min.x) * (bbox.max.y - bbox.min.y) * (bbox.max.z - bbox.min.z);
630
+ const volumeCm3 = Math.abs(volume) / 1000;
631
+
632
+ let estimate = {};
633
+
634
+ switch (process.toUpperCase()) {
635
+ case 'FDM':
636
+ estimate = {
637
+ process: 'FDM 3D Printing',
638
+ machine: MACHINE_PROFILES[machine]?.name || 'Generic FDM',
639
+ volumeCm3: round(volumeCm3, 2),
640
+ material,
641
+ materialWeight: round(volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24), 1),
642
+ materialCost: round(volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24) * (MATERIAL_COSTS[material] || 8.00) / 1000, 2),
643
+ machineTime: Math.round((volumeCm3 / 10) * 60),
644
+ machineCost: round((volumeCm3 / 10) * 0.5, 2),
645
+ setupCost: 0,
646
+ totalPerUnit: round(volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24) * (MATERIAL_COSTS[material] || 8.00) / 1000 + (volumeCm3 / 10) * 0.5, 2),
647
+ totalBatch: round(quantity * (volumeCm3 * (MATERIAL_DENSITIES[material] || 1.24) * (MATERIAL_COSTS[material] || 8.00) / 1000 + (volumeCm3 / 10) * 0.5), 2),
648
+ quantity
649
+ };
650
+ break;
651
+
652
+ case 'CNC':
653
+ const materialWeight = volumeCm3 * (MATERIAL_DENSITIES.aluminum || 2.70);
654
+ estimate = {
655
+ process: 'CNC Machining',
656
+ machine: MACHINE_PROFILES[machine]?.name || 'Generic CNC',
657
+ volumeCm3: round(volumeCm3, 2),
658
+ material: 'Aluminum 6061',
659
+ materialWeight: round(materialWeight, 1),
660
+ materialCost: round(materialWeight * 1.20 / 1000, 2),
661
+ machineTime: Math.round(volumeCm3 * 5),
662
+ machineCost: round(volumeCm3 * 0.5, 2),
663
+ setupCost: 25,
664
+ totalPerUnit: round(materialWeight * 1.20 / 1000 + volumeCm3 * 0.5 + 25 / quantity, 2),
665
+ totalBatch: round(quantity * (materialWeight * 1.20 / 1000 + volumeCm3 * 0.5) + 25, 2),
666
+ quantity
667
+ };
668
+ break;
669
+
670
+ case 'SLA':
671
+ estimate = {
672
+ process: 'SLA Resin Printing',
673
+ machine: MACHINE_PROFILES[machine]?.name || 'Generic SLA',
674
+ volumeCm3: round(volumeCm3, 2),
675
+ material: 'Standard Resin',
676
+ materialCost: round(volumeCm3 * 0.035, 2),
677
+ machineTime: Math.round((volumeCm3 / 10) * 40),
678
+ machineCost: round((volumeCm3 / 10) * 1.5, 2),
679
+ setupCost: 0,
680
+ totalPerUnit: round(volumeCm3 * 0.035 + (volumeCm3 / 10) * 1.5, 2),
681
+ totalBatch: round(quantity * (volumeCm3 * 0.035 + (volumeCm3 / 10) * 1.5), 2),
682
+ quantity
683
+ };
684
+ break;
685
+
686
+ default:
687
+ throw new Error(`Process "${process}" not supported. Use: FDM, CNC, SLA`);
688
+ }
689
+
690
+ estimate.timeMinutesReadable = `${Math.floor(estimate.machineTime / 60)}h ${estimate.machineTime % 60}m`;
691
+ estimate.currency = 'EUR';
692
+
693
+ return estimate;
694
+ },
695
+
696
+ /**
697
+ * Get available machines
698
+ */
699
+ getMachines: function() {
700
+ return Object.entries(MACHINE_PROFILES).map(([id, profile]) => ({
701
+ id,
702
+ ...profile
703
+ }));
704
+ },
705
+
706
+ /**
707
+ * Get available tools for CNC
708
+ */
709
+ getTools: function() {
710
+ return TOOL_LIBRARY;
711
+ },
712
+
713
+ /**
714
+ * Get material data
715
+ */
716
+ getMaterials: function() {
717
+ return {
718
+ materials: Object.keys(MATERIAL_DENSITIES).map(name => ({
719
+ name,
720
+ density: MATERIAL_DENSITIES[name],
721
+ costPerKg: MATERIAL_COSTS[name]
722
+ }))
723
+ };
724
+ }
725
+ };
726
+
727
+ // ============================================================================
728
+ // Helper: Generate G-code for slicing
729
+ // ============================================================================
730
+ function generateSlicingGcode(params) {
731
+ const { mesh, layerHeight, infill, shells, nozzleTemp, bedTemp, retraction, material } = params;
732
+ let gcode = [];
733
+
734
+ gcode.push('; Slicing G-code generated by cycleCAD');
735
+ gcode.push(`; Material: ${material}, Layer Height: ${layerHeight}mm, Infill: ${infill}%`);
736
+ gcode.push('G28 ; Home all axes');
737
+ gcode.push(`M140 S${bedTemp} ; Set bed temperature`);
738
+ gcode.push(`M104 S${nozzleTemp} ; Set nozzle temperature`);
739
+ gcode.push('M109 S' + nozzleTemp + ' ; Wait for nozzle');
740
+ gcode.push('M190 S' + bedTemp + ' ; Wait for bed');
741
+ gcode.push('G92 E0 ; Reset extruder');
742
+ gcode.push('G1 Z2.0 F3000 ; Lift nozzle');
743
+ gcode.push('G1 X10 Y10 F3000');
744
+ gcode.push('; --- Layer Data ---');
745
+
746
+ // Simplified layer generation
747
+ const bbox = new THREE.Box3().setFromObject(mesh);
748
+ const layers = Math.ceil((bbox.max.z - bbox.min.z) / layerHeight);
749
+
750
+ for (let layer = 0; layer < Math.min(layers, 20); layer++) {
751
+ gcode.push(`; Layer ${layer}`);
752
+ gcode.push(`G0 Z${(bbox.min.z + layer * layerHeight).toFixed(2)}`);
753
+ gcode.push(`G1 X${(bbox.min.x + 10).toFixed(1)} Y${(bbox.min.y + 10).toFixed(1)} E${(layer * 2).toFixed(1)} F3000`);
754
+ gcode.push(`G1 X${(bbox.max.x - 10).toFixed(1)} Y${(bbox.max.y - 10).toFixed(1)} E${((layer + 1) * 2).toFixed(1)} F3000`);
755
+ }
756
+
757
+ gcode.push('; --- End ---');
758
+ gcode.push('M104 S0 ; Turn off nozzle heater');
759
+ gcode.push('M140 S0 ; Turn off bed');
760
+ gcode.push('G28 X0 Y0 ; Home XY');
761
+ gcode.push('M84 ; Disable motors');
762
+
763
+ return gcode.join('\n');
764
+ }
765
+
766
+ // ============================================================================
767
+ // Helper: Generate G-code for CNC toolpath
768
+ // ============================================================================
769
+ function generateToolpathGcode(params) {
770
+ const { mesh, tool, strategy, depthPerPass, feedRate, spindle } = params;
771
+ let gcode = [];
772
+
773
+ gcode.push('; CNC Toolpath G-code generated by cycleCAD');
774
+ gcode.push(`; Tool: ${tool.name} (${tool.diameter}mm), Strategy: ${strategy}`);
775
+ gcode.push(`; Spindle: ${spindle} RPM, Feed: ${feedRate} mm/min`);
776
+ gcode.push('G90 ; Absolute positioning');
777
+ gcode.push('G94 ; Inches per minute feed');
778
+ gcode.push(`M3 S${spindle} ; Spindle on`);
779
+ gcode.push(`G0 X0 Y0 Z5 ; Move to start`);
780
+
781
+ const bbox = new THREE.Box3().setFromObject(mesh);
782
+ const passes = Math.ceil((bbox.max.z - bbox.min.z) / depthPerPass);
783
+
784
+ for (let pass = 0; pass < Math.min(passes, 10); pass++) {
785
+ const z = bbox.min.z + (pass + 1) * depthPerPass;
786
+ gcode.push(`; Pass ${pass + 1}`);
787
+ gcode.push(`G1 Z${z.toFixed(2)} F${feedRate}`);
788
+ gcode.push(`G1 X${(bbox.min.x + 10).toFixed(1)} Y${(bbox.min.y + 10).toFixed(1)} F${feedRate}`);
789
+ gcode.push(`G1 X${(bbox.max.x - 10).toFixed(1)} Y${(bbox.max.y - 10).toFixed(1)} F${feedRate}`);
790
+ gcode.push(`G0 Z5 F3000 ; Rapid retract`);
791
+ }
792
+
793
+ gcode.push('M5 ; Spindle off');
794
+ gcode.push('G0 X0 Y0 Z5 ; Return to home');
795
+
796
+ return gcode.join('\n');
797
+ }
798
+
799
+ // ============================================================================
800
+ // Helper: Generate SVG for nesting preview
801
+ // ============================================================================
802
+ function generateNestingSVG(sheetSize, placements, occupied) {
803
+ const scale = 0.2; // Scale down for preview
804
+ const svgWidth = sheetSize.width * scale;
805
+ const svgHeight = sheetSize.height * scale;
806
+
807
+ let svg = `<svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${sheetSize.width} ${sheetSize.height}" xmlns="http://www.w3.org/2000/svg">`;
808
+ svg += `<rect width="${sheetSize.width}" height="${sheetSize.height}" fill="#f5f5f5" stroke="#333" stroke-width="2"/>`;
809
+
810
+ // Draw placements
811
+ const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
812
+ placements.forEach((p, i) => {
813
+ const color = colors[i % colors.length];
814
+ svg += `<rect x="${p.x}" y="${p.y}" width="${p.width}" height="${p.height}" fill="${color}" opacity="0.7" stroke="#000" stroke-width="1"/>`;
815
+ svg += `<text x="${p.x + p.width/2}" y="${p.y + p.height/2}" text-anchor="middle" font-size="10" font-family="Arial">${p.partId}</text>`;
816
+ });
817
+
818
+ svg += '</svg>';
819
+ return svg;
820
+ }
821
+
822
+ // ============================================================================
823
+ // Helper: Utility functions
824
+ // ============================================================================
825
+ function round(num, decimals = 2) {
826
+ return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
827
+ }
828
+
829
+ // ============================================================================
830
+ // Export CAM API as window.cycleCAD.cam
831
+ // ============================================================================
832
+ if (!window.cycleCAD) {
833
+ window.cycleCAD = {};
834
+ }
835
+ window.cycleCAD.cam = camAPI;
836
+
837
+ console.log('[CAM Pipeline] Initialized. window.cycleCAD.cam ready.');
838
+ console.log('[CAM] Available commands:', Object.keys(camAPI).join(', '));
839
+
840
+ export default camAPI;