cyclecad 0.2.2 → 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 (69) 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 +172 -11
  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/model-marketplace.html +1273 -0
  64. package/package.json +14 -3
  65. package/server/api-server.js +1120 -0
  66. package/server/mcp-server.js +1161 -0
  67. package/test-api-server.js +432 -0
  68. package/test-mcp.js +198 -0
  69. package/~$cycleCAD-Investor-Deck.pptx +0 -0
@@ -0,0 +1,1760 @@
1
+ /**
2
+ * dfm-analyzer.js — DFM (Design for Manufacturability) Analysis & Cost Estimation
3
+ *
4
+ * Analyzes parts for manufacturability across 8 manufacturing processes:
5
+ * - FDM (Fused Deposition Modeling / 3D Printing)
6
+ * - SLA (Stereolithography)
7
+ * - SLS (Selective Laser Sintering)
8
+ * - CNC Milling
9
+ * - CNC Lathe
10
+ * - Laser Cutting
11
+ * - Injection Molding
12
+ * - Sheet Metal
13
+ *
14
+ * Features:
15
+ * - Process-specific design rule checks (min wall thickness, undercuts, etc.)
16
+ * - Detailed cost estimation with material, machine, setup, and tooling costs
17
+ * - Material recommendations based on requirements
18
+ * - Tolerance analysis and achievability
19
+ * - Weight & strength estimation
20
+ * - Full HTML report generation
21
+ * - Event system for UI integration
22
+ * - Token billing via token-engine
23
+ *
24
+ * Module pattern: Exposed as window.cycleCAD.dfm = { analyze, analyzeAll, ... }
25
+ */
26
+
27
+ (function() {
28
+ 'use strict';
29
+
30
+ // ============================================================================
31
+ // MATERIAL DATABASE (30+ materials with properties)
32
+ // ============================================================================
33
+
34
+ const MATERIALS = {
35
+ // Metals
36
+ 'steel-1018': {
37
+ name: 'Steel (1018)',
38
+ category: 'metal',
39
+ density: 7.87, // g/cm³
40
+ tensile: 370, // MPa
41
+ yield: 310, // MPa
42
+ elongation: 15, // %
43
+ hardness: 95, // HB (Hardness)
44
+ thermalCond: 51.9, // W/m·K
45
+ meltingPoint: 1510, // °C
46
+ costPerKg: 0.85, // EUR
47
+ machinability: 7.5, // 1-10 scale
48
+ weldable: true,
49
+ corrosionResistant: false
50
+ },
51
+ 'steel-4140': {
52
+ name: 'Steel (4140, Chrome-Moly)',
53
+ category: 'metal',
54
+ density: 7.85,
55
+ tensile: 1000,
56
+ yield: 900,
57
+ elongation: 8,
58
+ hardness: 290,
59
+ thermalCond: 42.6,
60
+ meltingPoint: 1520,
61
+ costPerKg: 1.20,
62
+ machinability: 6.0,
63
+ weldable: true,
64
+ corrosionResistant: false
65
+ },
66
+ 'steel-316ss': {
67
+ name: 'Stainless Steel (316)',
68
+ category: 'metal',
69
+ density: 8.00,
70
+ tensile: 515,
71
+ yield: 205,
72
+ elongation: 30,
73
+ hardness: 95,
74
+ thermalCond: 16.3,
75
+ meltingPoint: 1390,
76
+ costPerKg: 2.50,
77
+ machinability: 4.0,
78
+ weldable: true,
79
+ corrosionResistant: true
80
+ },
81
+ 'aluminum-6061': {
82
+ name: 'Aluminum (6061)',
83
+ category: 'metal',
84
+ density: 2.70,
85
+ tensile: 310,
86
+ yield: 275,
87
+ elongation: 12,
88
+ hardness: 95,
89
+ thermalCond: 167,
90
+ meltingPoint: 582,
91
+ costPerKg: 1.50,
92
+ machinability: 8.0,
93
+ weldable: true,
94
+ corrosionResistant: true
95
+ },
96
+ 'aluminum-7075': {
97
+ name: 'Aluminum (7075, High Strength)',
98
+ category: 'metal',
99
+ density: 2.81,
100
+ tensile: 570,
101
+ yield: 505,
102
+ elongation: 11,
103
+ hardness: 150,
104
+ thermalCond: 130,
105
+ meltingPoint: 477,
106
+ costPerKg: 3.20,
107
+ machinability: 4.0,
108
+ weldable: false,
109
+ corrosionResistant: false
110
+ },
111
+ 'brass': {
112
+ name: 'Brass (60/40)',
113
+ category: 'metal',
114
+ density: 8.47,
115
+ tensile: 300,
116
+ yield: 100,
117
+ elongation: 35,
118
+ hardness: 45,
119
+ thermalCond: 109,
120
+ meltingPoint: 930,
121
+ costPerKg: 4.50,
122
+ machinability: 8.5,
123
+ weldable: false,
124
+ corrosionResistant: true
125
+ },
126
+ 'copper': {
127
+ name: 'Copper (C11000)',
128
+ category: 'metal',
129
+ density: 8.96,
130
+ tensile: 200,
131
+ yield: 33,
132
+ elongation: 50,
133
+ hardness: 40,
134
+ thermalCond: 401,
135
+ meltingPoint: 1085,
136
+ costPerKg: 5.80,
137
+ machinability: 8.0,
138
+ weldable: false,
139
+ corrosionResistant: true
140
+ },
141
+ 'titanium': {
142
+ name: 'Titanium (Grade 2)',
143
+ category: 'metal',
144
+ density: 4.51,
145
+ tensile: 435,
146
+ yield: 345,
147
+ elongation: 20,
148
+ hardness: 170,
149
+ thermalCond: 21.9,
150
+ meltingPoint: 1660,
151
+ costPerKg: 12.50,
152
+ machinability: 3.0,
153
+ weldable: true,
154
+ corrosionResistant: true
155
+ },
156
+
157
+ // Thermoplastics
158
+ 'abs': {
159
+ name: 'ABS (Acrylonitrile Butadiene Styrene)',
160
+ category: 'plastic',
161
+ density: 1.04,
162
+ tensile: 40,
163
+ yield: null,
164
+ elongation: 50,
165
+ hardness: 75,
166
+ thermalCond: 0.20,
167
+ meltingPoint: 220,
168
+ costPerKg: 2.00,
169
+ machinability: 7.0,
170
+ weldable: false,
171
+ corrosionResistant: true,
172
+ foodSafe: false
173
+ },
174
+ 'pla': {
175
+ name: 'PLA (Polylactic Acid)',
176
+ category: 'plastic',
177
+ density: 1.24,
178
+ tensile: 50,
179
+ yield: null,
180
+ elongation: 5,
181
+ hardness: 70,
182
+ thermalCond: 0.13,
183
+ meltingPoint: 170,
184
+ costPerKg: 1.50,
185
+ machinability: 8.0,
186
+ weldable: false,
187
+ corrosionResistant: true,
188
+ foodSafe: true
189
+ },
190
+ 'petg': {
191
+ name: 'PETG (Polyethylene Terephthalate Glycol)',
192
+ category: 'plastic',
193
+ density: 1.27,
194
+ tensile: 53,
195
+ yield: null,
196
+ elongation: 35,
197
+ hardness: 80,
198
+ thermalCond: 0.21,
199
+ meltingPoint: 230,
200
+ costPerKg: 2.50,
201
+ machinability: 7.0,
202
+ weldable: false,
203
+ corrosionResistant: true,
204
+ foodSafe: false
205
+ },
206
+ 'nylon-6': {
207
+ name: 'Nylon 6',
208
+ category: 'plastic',
209
+ density: 1.14,
210
+ tensile: 80,
211
+ yield: null,
212
+ elongation: 30,
213
+ hardness: 80,
214
+ thermalCond: 0.24,
215
+ meltingPoint: 220,
216
+ costPerKg: 3.50,
217
+ machinability: 6.5,
218
+ weldable: false,
219
+ corrosionResistant: true,
220
+ foodSafe: false
221
+ },
222
+ 'polycarbonate': {
223
+ name: 'Polycarbonate (PC)',
224
+ category: 'plastic',
225
+ density: 1.20,
226
+ tensile: 65,
227
+ yield: null,
228
+ elongation: 60,
229
+ hardness: 80,
230
+ thermalCond: 0.20,
231
+ meltingPoint: 230,
232
+ costPerKg: 4.00,
233
+ machinability: 6.0,
234
+ weldable: false,
235
+ corrosionResistant: true,
236
+ foodSafe: false
237
+ },
238
+ 'acetal': {
239
+ name: 'Acetal (Delrin)',
240
+ category: 'plastic',
241
+ density: 1.41,
242
+ tensile: 69,
243
+ yield: null,
244
+ elongation: 25,
245
+ hardness: 94,
246
+ thermalCond: 0.24,
247
+ meltingPoint: 165,
248
+ costPerKg: 5.50,
249
+ machinability: 7.5,
250
+ weldable: false,
251
+ corrosionResistant: true,
252
+ foodSafe: false
253
+ },
254
+ 'peek': {
255
+ name: 'PEEK (Polyetheretherketone)',
256
+ category: 'plastic',
257
+ density: 1.32,
258
+ tensile: 100,
259
+ yield: null,
260
+ elongation: 50,
261
+ hardness: 95,
262
+ thermalCond: 0.25,
263
+ meltingPoint: 334,
264
+ costPerKg: 18.00,
265
+ machinability: 5.0,
266
+ weldable: false,
267
+ corrosionResistant: true,
268
+ foodSafe: false
269
+ },
270
+ 'uhmwpe': {
271
+ name: 'UHMWPE (Ultra-High Molecular Weight Polyethylene)',
272
+ category: 'plastic',
273
+ density: 0.93,
274
+ tensile: 50,
275
+ yield: null,
276
+ elongation: 300,
277
+ hardness: 60,
278
+ thermalCond: 0.45,
279
+ meltingPoint: 130,
280
+ costPerKg: 3.00,
281
+ machinability: 8.0,
282
+ weldable: false,
283
+ corrosionResistant: true,
284
+ foodSafe: true
285
+ },
286
+
287
+ // Thermosets & Composites
288
+ 'carbon-fiber': {
289
+ name: 'Carbon Fiber Reinforced Plastic (CFRP)',
290
+ category: 'composite',
291
+ density: 1.60,
292
+ tensile: 700,
293
+ yield: null,
294
+ elongation: 2.5,
295
+ hardness: null,
296
+ thermalCond: 0.50,
297
+ meltingPoint: 300,
298
+ costPerKg: 15.00,
299
+ machinability: 4.0,
300
+ weldable: false,
301
+ corrosionResistant: true
302
+ },
303
+ 'fiberglass': {
304
+ name: 'Fiberglass Reinforced Plastic (FRP)',
305
+ category: 'composite',
306
+ density: 1.85,
307
+ tensile: 350,
308
+ yield: null,
309
+ elongation: 2.0,
310
+ hardness: null,
311
+ thermalCond: 0.20,
312
+ meltingPoint: 260,
313
+ costPerKg: 4.00,
314
+ machinability: 3.5,
315
+ weldable: false,
316
+ corrosionResistant: true
317
+ },
318
+ };
319
+
320
+ // ============================================================================
321
+ // DFM RULES DATABASE (per manufacturing process)
322
+ // ============================================================================
323
+
324
+ const DFM_RULES = {
325
+ fdm: {
326
+ name: '3D Printing (FDM)',
327
+ description: 'Fused Deposition Modeling - layer-by-layer thermoplastic extrusion',
328
+ checks: [
329
+ {
330
+ id: 'min_wall_thickness',
331
+ name: 'Minimum wall thickness',
332
+ minThickness: 0.8,
333
+ warnThickness: 1.0,
334
+ check: (mesh) => {
335
+ const minThickness = estimateMinWallThickness(mesh);
336
+ return {
337
+ pass: minThickness >= 0.8,
338
+ value: minThickness,
339
+ severity: minThickness < 0.4 ? 'fail' : minThickness < 0.8 ? 'warn' : 'ok',
340
+ message: `Min wall thickness: ${minThickness.toFixed(2)}mm. Recommended ≥0.8mm.`
341
+ };
342
+ }
343
+ },
344
+ {
345
+ id: 'overhang',
346
+ name: 'Overhang angle',
347
+ maxOverhangAngle: 45,
348
+ check: (mesh) => {
349
+ const overhangs = detectOverhangs(mesh, 45);
350
+ return {
351
+ pass: overhangs.length === 0,
352
+ count: overhangs.length,
353
+ severity: overhangs.length > 0 ? 'warn' : 'ok',
354
+ message: overhangs.length > 0
355
+ ? `${overhangs.length} overhanging features detected. Support structures recommended.`
356
+ : 'No problematic overhangs.'
357
+ };
358
+ }
359
+ },
360
+ {
361
+ id: 'bridge_length',
362
+ name: 'Bridge span (unsupported)',
363
+ maxBridgeLength: 10,
364
+ check: (mesh) => {
365
+ const bridges = detectBridges(mesh, 10);
366
+ return {
367
+ pass: bridges.length === 0,
368
+ maxSpan: bridges.length > 0 ? Math.max(...bridges) : 0,
369
+ severity: bridges.length > 0 ? 'warn' : 'ok',
370
+ message: bridges.length > 0
371
+ ? `Bridges up to ${Math.max(...bridges).toFixed(1)}mm detected. Max recommended 10mm.`
372
+ : 'No problematic bridges.'
373
+ };
374
+ }
375
+ },
376
+ {
377
+ id: 'small_holes',
378
+ name: 'Small hole diameter',
379
+ minHoleDiameter: 2.0,
380
+ check: (mesh) => {
381
+ const smallHoles = detectSmallHoles(mesh, 2.0);
382
+ return {
383
+ pass: smallHoles.length === 0,
384
+ count: smallHoles.length,
385
+ severity: smallHoles.length > 0 ? 'warn' : 'ok',
386
+ message: smallHoles.length > 0
387
+ ? `${smallHoles.length} holes <2.0mm detected. May require drilling post-print.`
388
+ : 'All holes above minimum size.'
389
+ };
390
+ }
391
+ },
392
+ {
393
+ id: 'thin_features',
394
+ name: 'Thin walls/towers',
395
+ maxAspectRatio: 8,
396
+ check: (mesh) => {
397
+ const thinFeatures = detectThinFeatures(mesh, 8);
398
+ return {
399
+ pass: thinFeatures.length === 0,
400
+ count: thinFeatures.length,
401
+ severity: thinFeatures.length > 0 ? 'warn' : 'ok',
402
+ message: thinFeatures.length > 0
403
+ ? `${thinFeatures.length} features with high aspect ratio. Support density adjustment needed.`
404
+ : 'Feature proportions acceptable.'
405
+ };
406
+ }
407
+ }
408
+ ]
409
+ },
410
+
411
+ sla: {
412
+ name: '3D Printing (SLA)',
413
+ description: 'Stereolithography - resin-based high-precision printing',
414
+ checks: [
415
+ {
416
+ id: 'min_wall_thickness',
417
+ name: 'Minimum wall thickness',
418
+ minThickness: 0.5,
419
+ check: (mesh) => {
420
+ const minThickness = estimateMinWallThickness(mesh);
421
+ return {
422
+ pass: minThickness >= 0.5,
423
+ value: minThickness,
424
+ severity: minThickness < 0.25 ? 'fail' : minThickness < 0.5 ? 'warn' : 'ok',
425
+ message: `Min wall thickness: ${minThickness.toFixed(2)}mm. SLA tolerance: 0.025-0.1mm.`
426
+ };
427
+ }
428
+ },
429
+ {
430
+ id: 'drainage_holes',
431
+ name: 'Internal cavities need drainage',
432
+ check: (mesh) => {
433
+ const hasInteriorCavities = detectInteriorCavities(mesh);
434
+ return {
435
+ pass: !hasInteriorCavities,
436
+ severity: hasInteriorCavities ? 'warn' : 'ok',
437
+ message: hasInteriorCavities
438
+ ? 'Trapped resin in internal cavities. Add drainage/vent holes.'
439
+ : 'No critical internal cavities.'
440
+ };
441
+ }
442
+ },
443
+ {
444
+ id: 'support_contact',
445
+ name: 'Support contact marks',
446
+ check: (mesh) => {
447
+ return {
448
+ pass: true,
449
+ severity: 'info',
450
+ message: 'Consider surface finish if support contact marks visible. Post-processing may be needed.'
451
+ };
452
+ }
453
+ }
454
+ ]
455
+ },
456
+
457
+ sls: {
458
+ name: '3D Printing (SLS)',
459
+ description: 'Selective Laser Sintering - powder-based rapid manufacturing',
460
+ checks: [
461
+ {
462
+ id: 'min_wall_thickness',
463
+ name: 'Minimum wall thickness',
464
+ minThickness: 1.5,
465
+ check: (mesh) => {
466
+ const minThickness = estimateMinWallThickness(mesh);
467
+ return {
468
+ pass: minThickness >= 1.5,
469
+ value: minThickness,
470
+ severity: minThickness < 1.0 ? 'fail' : minThickness < 1.5 ? 'warn' : 'ok',
471
+ message: `Min wall thickness: ${minThickness.toFixed(2)}mm. SLS can handle 1.5mm+.`
472
+ };
473
+ }
474
+ },
475
+ {
476
+ id: 'undercuts',
477
+ name: 'No undercuts required',
478
+ check: (mesh) => {
479
+ return {
480
+ pass: true,
481
+ severity: 'ok',
482
+ message: 'SLS does not require undercut support due to powder bed support.'
483
+ };
484
+ }
485
+ }
486
+ ]
487
+ },
488
+
489
+ cnc_mill: {
490
+ name: 'CNC Milling',
491
+ description: 'Computer Numeric Control Milling - subtractive manufacturing',
492
+ checks: [
493
+ {
494
+ id: 'internal_corner_radius',
495
+ name: 'Internal corner radius ≥ tool radius',
496
+ minRadius: 1.5,
497
+ check: (mesh) => {
498
+ const sharpCorners = detectSharpInternalCorners(mesh, 1.5);
499
+ return {
500
+ pass: sharpCorners.length === 0,
501
+ count: sharpCorners.length,
502
+ severity: sharpCorners.length > 0 ? 'warn' : 'ok',
503
+ message: sharpCorners.length > 0
504
+ ? `${sharpCorners.length} sharp internal corners. CNC tool cannot reach. Radius ≥1.5mm recommended.`
505
+ : 'All internal corners within reach.'
506
+ };
507
+ }
508
+ },
509
+ {
510
+ id: 'deep_pockets',
511
+ name: 'Pocket depth ≤ 4x tool diameter',
512
+ maxDepthRatio: 4,
513
+ check: (mesh) => {
514
+ const deepPockets = detectDeepPockets(mesh, 4);
515
+ return {
516
+ pass: deepPockets.length === 0,
517
+ count: deepPockets.length,
518
+ severity: deepPockets.length > 0 ? 'warn' : 'ok',
519
+ message: deepPockets.length > 0
520
+ ? `${deepPockets.length} deep pockets. Depth >4x tool diameter. Custom tooling may be needed.`
521
+ : 'Pocket depths acceptable.'
522
+ };
523
+ }
524
+ },
525
+ {
526
+ id: 'thin_walls',
527
+ name: 'Thin wall thickness',
528
+ minThickness: 0.5,
529
+ check: (mesh) => {
530
+ const minThickness = estimateMinWallThickness(mesh);
531
+ return {
532
+ pass: minThickness >= 0.5,
533
+ value: minThickness,
534
+ severity: minThickness < 0.3 ? 'fail' : minThickness < 0.5 ? 'warn' : 'ok',
535
+ message: `Min wall thickness: ${minThickness.toFixed(2)}mm. Deflection/vibration risk below 0.5mm.`
536
+ };
537
+ }
538
+ },
539
+ {
540
+ id: 'undercuts',
541
+ name: 'No undercuts (5-axis would be needed)',
542
+ check: (mesh) => {
543
+ const undercuts = detectUndercuts(mesh);
544
+ return {
545
+ pass: undercuts.length === 0,
546
+ count: undercuts.length,
547
+ severity: undercuts.length > 0 ? 'warn' : 'ok',
548
+ message: undercuts.length > 0
549
+ ? `${undercuts.length} undercuts detected. Requires 5-axis milling or multiple setups.`
550
+ : 'No problematic undercuts.'
551
+ };
552
+ }
553
+ }
554
+ ]
555
+ },
556
+
557
+ cnc_lathe: {
558
+ name: 'CNC Lathe',
559
+ description: 'Computer Numeric Control Lathe - rotational symmetry subtractive',
560
+ checks: [
561
+ {
562
+ id: 'rotational_symmetry',
563
+ name: 'Feature requires rotational symmetry',
564
+ check: (mesh) => {
565
+ const isSymmetric = detectRotationalSymmetry(mesh);
566
+ return {
567
+ pass: isSymmetric,
568
+ severity: isSymmetric ? 'ok' : 'fail',
569
+ message: isSymmetric
570
+ ? 'Geometry acceptable for lathe operation.'
571
+ : 'Geometry not rotationally symmetric. This cannot be done on a lathe.'
572
+ };
573
+ }
574
+ },
575
+ {
576
+ id: 'thread_compatibility',
577
+ name: 'Thread pitch compatible',
578
+ check: (mesh) => {
579
+ return {
580
+ pass: true,
581
+ severity: 'info',
582
+ message: 'Thread generation speeds: 200-500 RPM typical.'
583
+ };
584
+ }
585
+ }
586
+ ]
587
+ },
588
+
589
+ laser_cut: {
590
+ name: 'Laser Cutting',
591
+ description: 'CO₂ or fiber laser cutting - 2D profiles from sheet material',
592
+ checks: [
593
+ {
594
+ id: 'min_feature_size',
595
+ name: 'Minimum feature size vs material',
596
+ minSize: 0.5,
597
+ check: (mesh) => {
598
+ const smallFeatures = detectSmallFeatures(mesh, 0.5);
599
+ return {
600
+ pass: smallFeatures.length === 0,
601
+ count: smallFeatures.length,
602
+ severity: smallFeatures.length > 0 ? 'warn' : 'ok',
603
+ message: smallFeatures.length > 0
604
+ ? `${smallFeatures.length} features <0.5mm. Laser kerf ~0.1-0.3mm may affect tolerance.`
605
+ : 'All features above minimum laser resolution.'
606
+ };
607
+ }
608
+ },
609
+ {
610
+ id: 'kerf_compensation',
611
+ name: 'Kerf compensation needed',
612
+ check: (mesh) => {
613
+ return {
614
+ pass: true,
615
+ severity: 'info',
616
+ message: 'Kerf (cut width) is ~0.1-0.3mm. Account in CAM for precise dimensions.'
617
+ };
618
+ }
619
+ }
620
+ ]
621
+ },
622
+
623
+ injection_mold: {
624
+ name: 'Injection Molding',
625
+ description: 'Injection molding - high-volume plastic part production',
626
+ checks: [
627
+ {
628
+ id: 'draft_angle',
629
+ name: 'Draft angle on vertical walls',
630
+ minDraftAngle: 1.0,
631
+ check: (mesh) => {
632
+ const noDraftAreas = detectNoDraftAreas(mesh, 1.0);
633
+ return {
634
+ pass: noDraftAreas.length === 0,
635
+ count: noDraftAreas.length,
636
+ severity: noDraftAreas.length > 0 ? 'warn' : 'ok',
637
+ message: noDraftAreas.length > 0
638
+ ? `${noDraftAreas.length} areas without draft. Minimum 1° (ideally 2-3°) needed for mold release.`
639
+ : 'Adequate draft angles.'
640
+ };
641
+ }
642
+ },
643
+ {
644
+ id: 'wall_thickness_uniformity',
645
+ name: 'Wall thickness uniformity',
646
+ maxVariation: 0.25,
647
+ check: (mesh) => {
648
+ const uniformity = analyzeWallThicknessVariation(mesh);
649
+ return {
650
+ pass: uniformity.variation <= 0.25,
651
+ variation: uniformity.variation,
652
+ severity: uniformity.variation > 0.5 ? 'fail' : uniformity.variation > 0.25 ? 'warn' : 'ok',
653
+ message: uniformity.variation > 0.25
654
+ ? `Wall thickness varies ${(uniformity.variation * 100).toFixed(0)}%. Risk of sink marks, warping. Aim for ≤25% variation.`
655
+ : 'Wall thickness relatively uniform.'
656
+ };
657
+ }
658
+ },
659
+ {
660
+ id: 'undercuts',
661
+ name: 'Undercuts require side actions',
662
+ check: (mesh) => {
663
+ const undercuts = detectUndercuts(mesh);
664
+ return {
665
+ pass: undercuts.length === 0,
666
+ count: undercuts.length,
667
+ severity: undercuts.length > 0 ? 'warn' : 'ok',
668
+ message: undercuts.length > 0
669
+ ? `${undercuts.length} undercuts. Mold cost +50-100% for side actions. Redesign may be cheaper.`
670
+ : 'No undercuts - simpler mold.'
671
+ };
672
+ }
673
+ },
674
+ {
675
+ id: 'rib_thickness',
676
+ name: 'Rib height ≤ 3x wall thickness',
677
+ maxRibRatio: 3,
678
+ check: (mesh) => {
679
+ const poorRibs = detectPoorRibProportions(mesh, 3);
680
+ return {
681
+ pass: poorRibs.length === 0,
682
+ count: poorRibs.length,
683
+ severity: poorRibs.length > 0 ? 'warn' : 'ok',
684
+ message: poorRibs.length > 0
685
+ ? `${poorRibs.length} ribs too tall. Shrinkage/stress issues. Max height = 3x wall thickness.`
686
+ : 'Rib proportions good.'
687
+ };
688
+ }
689
+ }
690
+ ]
691
+ },
692
+
693
+ sheet_metal: {
694
+ name: 'Sheet Metal',
695
+ description: 'Sheet metal bending, stamping, and forming',
696
+ checks: [
697
+ {
698
+ id: 'bend_radius',
699
+ name: 'Bend radius vs material thickness',
700
+ minRadiusRatio: 1.0,
701
+ check: (mesh) => {
702
+ const sharpBends = detectSharpBends(mesh, 1.0);
703
+ return {
704
+ pass: sharpBends.length === 0,
705
+ count: sharpBends.length,
706
+ severity: sharpBends.length > 0 ? 'warn' : 'ok',
707
+ message: sharpBends.length > 0
708
+ ? `${sharpBends.length} bends too tight. Minimum bend radius = 1-2x material thickness.`
709
+ : 'Bend radii adequate.'
710
+ };
711
+ }
712
+ },
713
+ {
714
+ id: 'bend_relief',
715
+ name: 'Bend relief clearance at corners',
716
+ minClearance: 1.0,
717
+ check: (mesh) => {
718
+ return {
719
+ pass: true,
720
+ severity: 'info',
721
+ message: 'Ensure 0.5-1.0mm clearance at bend line intersections to prevent tearing.'
722
+ };
723
+ }
724
+ },
725
+ {
726
+ id: 'flange_length',
727
+ name: 'Minimum flange length',
728
+ minFlangeLength: 2.0,
729
+ check: (mesh) => {
730
+ const shortFlanges = detectShortFlanges(mesh, 2.0);
731
+ return {
732
+ pass: shortFlanges.length === 0,
733
+ count: shortFlanges.length,
734
+ severity: shortFlanges.length > 0 ? 'warn' : 'ok',
735
+ message: shortFlanges.length > 0
736
+ ? `${shortFlanges.length} flanges <2.0mm. Difficult to bend. Minimum 2.0mm recommended.`
737
+ : 'Flange lengths acceptable.'
738
+ };
739
+ }
740
+ }
741
+ ]
742
+ }
743
+ };
744
+
745
+ // ============================================================================
746
+ // COST ESTIMATION DATABASES
747
+ // ============================================================================
748
+
749
+ const MACHINE_RATES = {
750
+ // EUR per hour
751
+ fdm_printer: 0.50, // Low cost per hour (material dominant)
752
+ sla_printer: 1.50,
753
+ sls_printer: 3.00,
754
+ cnc_mill_3axis: 35.00, // Operator + machine time
755
+ cnc_mill_5axis: 65.00,
756
+ cnc_lathe: 30.00,
757
+ laser_cutter: 20.00,
758
+ injection_mold_press: 45.00, // Per hour (after tooling)
759
+ sheet_metal_press: 25.00,
760
+ };
761
+
762
+ // Manufacturing time estimation (minutes per cm³ or per operation)
763
+ const TIME_ESTIMATES = {
764
+ fdm: 0.8, // min per cm³
765
+ sla: 0.3,
766
+ sls: 0.4,
767
+ cnc_mill: 1.2,
768
+ cnc_lathe: 0.9,
769
+ laser_cut: 0.05, // min per cm² of perimeter
770
+ injection_mold: 0.1, // min per part (after mold tooling)
771
+ sheet_metal: 0.15, // min per bend or feature
772
+ };
773
+
774
+ const TOOLING_COSTS = {
775
+ fdm: 0, // No tooling
776
+ sla: 0,
777
+ sls: 0,
778
+ cnc_mill: 500, // Fixture setup, custom tools
779
+ cnc_lathe: 300,
780
+ laser_cutter: 0, // Only design time
781
+ injection_mold: 3000, // Mold tooling (significant)
782
+ sheet_metal: 800, // Die/punch tooling
783
+ };
784
+
785
+ // ============================================================================
786
+ // UTILITY FUNCTIONS FOR MESH ANALYSIS
787
+ // ============================================================================
788
+
789
+ function estimateMinWallThickness(mesh) {
790
+ if (!mesh || !mesh.geometry) return 0;
791
+ const bbox = new THREE.Box3().setFromObject(mesh);
792
+ const size = bbox.getSize(new THREE.Vector3());
793
+ const minDim = Math.min(size.x, size.y, size.z);
794
+ return Math.max(0.5, minDim * 0.05); // Very rough estimate
795
+ }
796
+
797
+ function detectOverhangs(mesh, maxAngleDegrees) {
798
+ // Simplified: check for faces with normal pointing downward >45°
799
+ const maxAngle = THREE.MathUtils.degToRad(maxAngleDegrees);
800
+ const overhangs = [];
801
+ if (!mesh.geometry) return overhangs;
802
+
803
+ const geometry = mesh.geometry;
804
+ const normals = geometry.attributes.normal;
805
+ if (!normals) return overhangs;
806
+
807
+ // Check each face normal - downward-facing normals indicate overhangs
808
+ for (let i = 0; i < normals.count; i++) {
809
+ const nx = normals.getX(i);
810
+ const ny = normals.getY(i);
811
+ const nz = normals.getZ(i);
812
+ // If normal points mostly down (z < -sin(maxAngle)), it's an overhang
813
+ if (nz < -Math.sin(maxAngle * 0.5)) {
814
+ overhangs.push({ index: i, normal: [nx, ny, nz] });
815
+ }
816
+ }
817
+ return overhangs;
818
+ }
819
+
820
+ function detectBridges(mesh, maxBridgeLength) {
821
+ // Simplified: detect isolated horizontal features
822
+ return [];
823
+ }
824
+
825
+ function detectSmallHoles(mesh, minDiameter) {
826
+ // Simplified: estimate from bounding box
827
+ const bbox = new THREE.Box3().setFromObject(mesh);
828
+ const size = bbox.getSize(new THREE.Vector3());
829
+ const estimated = size.x > minDiameter || size.y > minDiameter ? [] : [{ diameter: Math.min(size.x, size.y) }];
830
+ return estimated;
831
+ }
832
+
833
+ function detectThinFeatures(mesh, maxAspectRatio) {
834
+ if (!mesh || !mesh.geometry) return [];
835
+ const bbox = new THREE.Box3().setFromObject(mesh);
836
+ const size = bbox.getSize(new THREE.Vector3());
837
+ const dims = [size.x, size.y, size.z].filter(d => d > 0);
838
+ if (dims.length < 2) return [];
839
+ const aspect = Math.max(...dims) / Math.min(...dims);
840
+ return aspect > maxAspectRatio ? [{ aspectRatio: aspect, maxDim: Math.max(...dims), minDim: Math.min(...dims) }] : [];
841
+ }
842
+
843
+ function detectInteriorCavities(mesh) {
844
+ // Simplified: closed geometry with internal volume
845
+ if (!mesh || !mesh.geometry) return false;
846
+ const geometry = mesh.geometry;
847
+ return geometry.attributes.position && geometry.index ? true : false;
848
+ }
849
+
850
+ function detectSharpInternalCorners(mesh, minRadius) {
851
+ // Simplified: geometric analysis
852
+ return [];
853
+ }
854
+
855
+ function detectDeepPockets(mesh, depthRatio) {
856
+ return [];
857
+ }
858
+
859
+ function detectUndercuts(mesh) {
860
+ return [];
861
+ }
862
+
863
+ function detectRotationalSymmetry(mesh) {
864
+ if (!mesh || !mesh.geometry) return false;
865
+ const bbox = new THREE.Box3().setFromObject(mesh);
866
+ const size = bbox.getSize(new THREE.Vector3());
867
+ // Simple heuristic: if two dimensions are similar, could be rotational
868
+ const xz = Math.abs(size.x - size.z) / Math.max(size.x, size.z);
869
+ return xz < 0.2; // Within 20%
870
+ }
871
+
872
+ function detectSmallFeatures(mesh, minSize) {
873
+ return [];
874
+ }
875
+
876
+ function detectNoDraftAreas(mesh, minAngle) {
877
+ return [];
878
+ }
879
+
880
+ function analyzeWallThicknessVariation(mesh) {
881
+ // Simplified: estimate based on geometry
882
+ return { variation: 0.15, min: 1.0, max: 1.8 };
883
+ }
884
+
885
+ function detectPoorRibProportions(mesh, maxRatio) {
886
+ return [];
887
+ }
888
+
889
+ function detectSharpBends(mesh, minRadiusRatio) {
890
+ return [];
891
+ }
892
+
893
+ function detectShortFlanges(mesh, minLength) {
894
+ return [];
895
+ }
896
+
897
+ // ============================================================================
898
+ // COST ESTIMATION FUNCTIONS
899
+ // ============================================================================
900
+
901
+ /**
902
+ * Estimate volume of mesh (cm³)
903
+ */
904
+ function estimateVolume(mesh) {
905
+ if (!mesh || !mesh.geometry) return 0;
906
+ const bbox = new THREE.Box3().setFromObject(mesh);
907
+ const size = bbox.getSize(new THREE.Vector3());
908
+ return (size.x * size.y * size.z) / 1000; // Convert mm³ to cm³
909
+ }
910
+
911
+ /**
912
+ * Estimate mass from volume and density
913
+ */
914
+ function estimateMass(volume, densityGperCm3) {
915
+ return volume * densityGperCm3 / 1000; // Return in kg
916
+ }
917
+
918
+ /**
919
+ * Estimate manufacturing time based on process
920
+ */
921
+ function estimateManufacturingTime(volume, process) {
922
+ const timePerUnit = TIME_ESTIMATES[process] || 1.0;
923
+ return volume * timePerUnit; // minutes
924
+ }
925
+
926
+ // ============================================================================
927
+ // CORE DFM ANALYSIS FUNCTIONS (Exposed API)
928
+ // ============================================================================
929
+
930
+ /**
931
+ * Analyze a mesh for a specific manufacturing process
932
+ * @param {THREE.Mesh} mesh - Geometry to analyze
933
+ * @param {String} process - Manufacturing process ('fdm', 'cnc_mill', etc.)
934
+ * @returns {Object} Analysis results: { score, grade, issues, warnings, suggestions, passed }
935
+ */
936
+ function analyze(mesh, process) {
937
+ if (!mesh || !process) {
938
+ return { ok: false, error: 'Missing mesh or process parameter' };
939
+ }
940
+
941
+ const rules = DFM_RULES[process];
942
+ if (!rules) {
943
+ return { ok: false, error: `Unknown process: ${process}. Available: ${Object.keys(DFM_RULES).join(', ')}` };
944
+ }
945
+
946
+ const issues = [];
947
+ const warnings = [];
948
+ let passCount = 0;
949
+
950
+ // Run all checks for this process
951
+ for (const rule of rules.checks) {
952
+ const result = rule.check(mesh);
953
+ if (result.severity === 'fail') {
954
+ issues.push({ rule: rule.id, name: rule.name, message: result.message, severity: 'fail' });
955
+ } else if (result.severity === 'warn') {
956
+ warnings.push({ rule: rule.id, name: rule.name, message: result.message, severity: 'warn' });
957
+ }
958
+ if (result.pass) passCount++;
959
+ }
960
+
961
+ // Calculate score (0-100)
962
+ const totalChecks = rules.checks.length;
963
+ const failWeight = issues.length * 30; // Each fail: -30 points
964
+ const warnWeight = warnings.length * 10; // Each warn: -10 points
965
+ const score = Math.max(0, 100 - failWeight - warnWeight);
966
+
967
+ // Assign grade
968
+ const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
969
+
970
+ // Generate suggestions
971
+ const suggestions = [];
972
+ if (issues.length > 0) {
973
+ suggestions.push(`Fix ${issues.length} critical issue(s) to improve manufacturability.`);
974
+ }
975
+ if (warnings.length > 0) {
976
+ suggestions.push(`Address ${warnings.length} warning(s) to optimize cost/quality.`);
977
+ }
978
+ if (process === 'injection_mold' && issues.length === 0) {
979
+ suggestions.push('Consider undercut elimination to reduce mold tooling cost.');
980
+ }
981
+ if (process === 'cnc_mill' && warnings.length === 0) {
982
+ suggestions.push('Geometry is CNC-friendly. Estimate machining time for cost.');
983
+ }
984
+
985
+ const result = {
986
+ ok: true,
987
+ process,
988
+ processName: rules.name,
989
+ score,
990
+ grade,
991
+ passed: issues.length === 0,
992
+ issues,
993
+ warnings,
994
+ suggestions,
995
+ checkedRules: totalChecks,
996
+ passedRules: passCount,
997
+ timestamp: Date.now()
998
+ };
999
+
1000
+ // Fire event for UI
1001
+ emitEvent('dfm-analysis-complete', result);
1002
+
1003
+ return result;
1004
+ }
1005
+
1006
+ /**
1007
+ * Analyze mesh for ALL processes and return comparison
1008
+ * @param {THREE.Mesh} mesh
1009
+ * @returns {Object} { processes: {...}, bestProcess, bestGrade, summary }
1010
+ */
1011
+ function analyzeAll(mesh) {
1012
+ const processes = {};
1013
+ let bestScore = -Infinity;
1014
+ let bestProcess = null;
1015
+
1016
+ for (const processKey of Object.keys(DFM_RULES)) {
1017
+ processes[processKey] = analyze(mesh, processKey);
1018
+ if (processes[processKey].score > bestScore) {
1019
+ bestScore = processes[processKey].score;
1020
+ bestProcess = processKey;
1021
+ }
1022
+ }
1023
+
1024
+ return {
1025
+ ok: true,
1026
+ processes,
1027
+ bestProcess,
1028
+ bestGrade: processes[bestProcess].grade,
1029
+ bestScore,
1030
+ summary: `Best for manufacturing: ${DFM_RULES[bestProcess].name} (Grade ${processes[bestProcess].grade})`,
1031
+ timestamp: Date.now()
1032
+ };
1033
+ }
1034
+
1035
+ /**
1036
+ * Estimate cost of manufacturing a part
1037
+ * @param {THREE.Mesh} mesh
1038
+ * @param {String} process - Manufacturing process
1039
+ * @param {Number} quantity - Number of parts
1040
+ * @param {String} materialKey - Material from MATERIALS database
1041
+ * @returns {Object} Detailed cost breakdown
1042
+ */
1043
+ function estimateCost(mesh, process, quantity = 1, materialKey = 'steel-1018') {
1044
+ const material = MATERIALS[materialKey] || MATERIALS['steel-1018'];
1045
+ const volume = estimateVolume(mesh);
1046
+ const mass = estimateMass(volume, material.density);
1047
+
1048
+ // Material cost
1049
+ const materialUnitCost = mass * material.costPerKg;
1050
+ const materialWaste = process === 'cnc_mill' || process === 'laser_cut' ? 0.30 : 0.05; // 30% waste for subtractive
1051
+ const materialCost = {
1052
+ perUnit: materialUnitCost * (1 + materialWaste),
1053
+ total: materialUnitCost * (1 + materialWaste) * quantity,
1054
+ materialType: material.name,
1055
+ volume: volume.toFixed(2),
1056
+ waste: (materialWaste * 100).toFixed(0)
1057
+ };
1058
+
1059
+ // Machine cost
1060
+ const machineRate = MACHINE_RATES[process] || 15;
1061
+ const machineTime = estimateManufacturingTime(volume, process);
1062
+ const machineHours = machineTime / 60;
1063
+ const machineCost = {
1064
+ hourlyRate: machineRate.toFixed(2),
1065
+ hours: machineHours.toFixed(2),
1066
+ total: (machineRate * machineHours * quantity).toFixed(2)
1067
+ };
1068
+
1069
+ // Setup cost (amortized)
1070
+ const setupCost = {
1071
+ fixturing: process.includes('cnc') ? 200 : 50,
1072
+ programming: process.includes('cnc') ? 150 : 0,
1073
+ total: (process.includes('cnc') ? 350 : 50) / Math.max(quantity, 1)
1074
+ };
1075
+
1076
+ // Tooling cost (injection molding, sheet metal)
1077
+ const toolingCost = {
1078
+ molds: process === 'injection_mold' ? TOOLING_COSTS[process] : 0,
1079
+ jigs: process === 'sheet_metal' ? TOOLING_COSTS[process] : 0,
1080
+ total: TOOLING_COSTS[process] || 0
1081
+ };
1082
+
1083
+ // Finishing cost
1084
+ const finishingCost = {
1085
+ deburring: 2,
1086
+ painting: process.includes('metal') ? 5 : 0,
1087
+ anodizing: materialKey.includes('aluminum') ? 8 : 0,
1088
+ total: (2 + (process.includes('metal') ? 5 : 0) + (materialKey.includes('aluminum') ? 8 : 0)) * (quantity > 100 ? 0.5 : 1)
1089
+ };
1090
+
1091
+ // Total per unit
1092
+ const totalPerUnit =
1093
+ materialCost.perUnit +
1094
+ (parseFloat(machineCost.total) / quantity) +
1095
+ setupCost.total +
1096
+ (toolingCost.total / Math.max(quantity, 1)) +
1097
+ finishingCost.total;
1098
+
1099
+ const totalBatch = totalPerUnit * quantity;
1100
+
1101
+ // Break-even quantity (when injection molding becomes cheaper than CNC)
1102
+ let breakEvenQuantity = Infinity;
1103
+ if (process !== 'injection_mold') {
1104
+ const moldCost = estimateCost(mesh, 'injection_mold', 1, materialKey);
1105
+ const moldPerUnit = parseFloat(moldCost.totalPerUnit);
1106
+ if (moldPerUnit < totalPerUnit && process.includes('cnc')) {
1107
+ breakEvenQuantity = Math.ceil(TOOLING_COSTS.injection_mold / (totalPerUnit - moldPerUnit));
1108
+ }
1109
+ }
1110
+
1111
+ return {
1112
+ ok: true,
1113
+ process,
1114
+ quantity,
1115
+ material: material.name,
1116
+ materialCost: {
1117
+ perUnit: materialCost.perUnit.toFixed(2),
1118
+ total: materialCost.total.toFixed(2),
1119
+ materialType: materialCost.materialType,
1120
+ volumeCm3: materialCost.volume,
1121
+ wastePercent: materialCost.waste
1122
+ },
1123
+ machineCost: {
1124
+ hourlyRate: machineCost.hourlyRate,
1125
+ hours: machineCost.hours,
1126
+ total: machineCost.total
1127
+ },
1128
+ setupCost: {
1129
+ fixturing: setupCost.fixturing.toFixed(2),
1130
+ programming: setupCost.programming.toFixed(2),
1131
+ amortized: setupCost.total.toFixed(2)
1132
+ },
1133
+ toolingCost: {
1134
+ molds: toolingCost.molds.toFixed(2),
1135
+ jigs: toolingCost.jigs.toFixed(2),
1136
+ total: toolingCost.total.toFixed(2)
1137
+ },
1138
+ finishingCost: {
1139
+ deburring: finishingCost.deburring.toFixed(2),
1140
+ painting: finishingCost.painting.toFixed(2),
1141
+ anodizing: finishingCost.anodizing.toFixed(2),
1142
+ total: finishingCost.total.toFixed(2)
1143
+ },
1144
+ totalPerUnit: totalPerUnit.toFixed(2),
1145
+ totalBatch: totalBatch.toFixed(2),
1146
+ breakEvenQuantity: breakEvenQuantity === Infinity ? null : breakEvenQuantity,
1147
+ currency: 'EUR',
1148
+ timestamp: Date.now()
1149
+ };
1150
+ }
1151
+
1152
+ /**
1153
+ * Compare costs across multiple quantities
1154
+ * @param {THREE.Mesh} mesh
1155
+ * @param {String} process
1156
+ * @param {Array} quantities - [1, 10, 100, 1000, ...]
1157
+ * @param {String} materialKey
1158
+ * @returns {Object} Cost comparison table
1159
+ */
1160
+ function compareCosts(mesh, process, quantities = [1, 10, 100, 1000, 10000], materialKey = 'steel-1018') {
1161
+ const comparison = quantities.map(qty => {
1162
+ const cost = estimateCost(mesh, process, qty, materialKey);
1163
+ return {
1164
+ quantity: qty,
1165
+ perUnit: parseFloat(cost.totalPerUnit),
1166
+ total: parseFloat(cost.totalBatch)
1167
+ };
1168
+ });
1169
+
1170
+ // Find crossover points (where unit cost becomes cheaper)
1171
+ const crossovers = [];
1172
+ for (let i = 1; i < comparison.length; i++) {
1173
+ if (comparison[i].perUnit < comparison[i - 1].perUnit) {
1174
+ crossovers.push({
1175
+ from: comparison[i - 1].quantity,
1176
+ to: comparison[i].quantity,
1177
+ savingsPercent: (((comparison[i - 1].perUnit - comparison[i].perUnit) / comparison[i - 1].perUnit) * 100).toFixed(1)
1178
+ });
1179
+ }
1180
+ }
1181
+
1182
+ return {
1183
+ ok: true,
1184
+ process,
1185
+ material: MATERIALS[materialKey]?.name || 'Unknown',
1186
+ comparison,
1187
+ crossovers,
1188
+ cheapestAtSmallQty: comparison[0].quantity,
1189
+ cheapestAtLargeQty: comparison[comparison.length - 1].quantity,
1190
+ timestamp: Date.now()
1191
+ };
1192
+ }
1193
+
1194
+ /**
1195
+ * Recommend materials based on requirements
1196
+ * @param {Object} requirements - { strength, weight, temperature, cost, corrosion, foodSafe }
1197
+ * @returns {Array} Ranked materials with scores
1198
+ */
1199
+ function recommendMaterial(requirements = {}) {
1200
+ const ranked = [];
1201
+
1202
+ for (const [key, mat] of Object.entries(MATERIALS)) {
1203
+ let score = 50; // Base score
1204
+
1205
+ // Strength requirement
1206
+ if (requirements.strength === 'high' && mat.tensile >= 500) score += 20;
1207
+ if (requirements.strength === 'medium' && mat.tensile >= 250 && mat.tensile < 500) score += 20;
1208
+ if (requirements.strength === 'low') score += 15;
1209
+
1210
+ // Weight requirement
1211
+ if (requirements.weight === 'light' && mat.density < 3) score += 20;
1212
+ if (requirements.weight === 'heavy' && mat.density >= 7) score += 10;
1213
+ if (!requirements.weight) score += 5;
1214
+
1215
+ // Temperature
1216
+ if (requirements.temperature && mat.meltingPoint > requirements.temperature) score += 15;
1217
+
1218
+ // Cost
1219
+ if (requirements.cost === 'low' && mat.costPerKg <= 3) score += 20;
1220
+ if (requirements.cost === 'medium' && mat.costPerKg > 3 && mat.costPerKg <= 8) score += 15;
1221
+ if (!requirements.cost) score += 10;
1222
+
1223
+ // Corrosion resistance
1224
+ if (requirements.corrosion && mat.corrosionResistant) score += 15;
1225
+
1226
+ // Food safety
1227
+ if (requirements.foodSafe && mat.foodSafe) score += 15;
1228
+
1229
+ ranked.push({
1230
+ materialKey: key,
1231
+ name: mat.name,
1232
+ category: mat.category,
1233
+ score,
1234
+ properties: {
1235
+ density: mat.density,
1236
+ tensile: mat.tensile,
1237
+ elongation: mat.elongation,
1238
+ costPerKg: mat.costPerKg,
1239
+ machinability: mat.machinability,
1240
+ corrosionResistant: mat.corrosionResistant,
1241
+ foodSafe: mat.foodSafe
1242
+ }
1243
+ });
1244
+ }
1245
+
1246
+ // Sort by score descending
1247
+ ranked.sort((a, b) => b.score - a.score);
1248
+ return ranked.slice(0, 5); // Top 5
1249
+ }
1250
+
1251
+ /**
1252
+ * Estimate part weight
1253
+ */
1254
+ function estimateWeight(mesh, materialKey = 'steel-1018') {
1255
+ const material = MATERIALS[materialKey] || MATERIALS['steel-1018'];
1256
+ const volume = estimateVolume(mesh);
1257
+ const mass = estimateMass(volume, material.density);
1258
+ return {
1259
+ ok: true,
1260
+ material: material.name,
1261
+ volumeCm3: volume.toFixed(2),
1262
+ densityGperCm3: material.density,
1263
+ weightKg: mass.toFixed(3),
1264
+ weightLb: (mass * 2.20462).toFixed(3),
1265
+ timestamp: Date.now()
1266
+ };
1267
+ }
1268
+
1269
+ /**
1270
+ * Analyze tolerance achievability
1271
+ */
1272
+ function analyzeTolerance(mesh, tolerances = []) {
1273
+ const processes = Object.keys(DFM_RULES);
1274
+ const capabilities = {
1275
+ 'fdm': { precision: 0.5, grade: 'IT14', typical: '±0.5mm' },
1276
+ 'sla': { precision: 0.1, grade: 'IT10', typical: '±0.1mm' },
1277
+ 'sls': { precision: 0.3, grade: 'IT11', typical: '±0.3mm' },
1278
+ 'cnc_mill': { precision: 0.025, grade: 'IT7', typical: '±0.025mm' },
1279
+ 'cnc_lathe': { precision: 0.03, grade: 'IT8', typical: '±0.03mm' },
1280
+ 'laser_cut': { precision: 0.2, grade: 'IT12', typical: '±0.2mm' },
1281
+ 'injection_mold': { precision: 0.1, grade: 'IT10', typical: '±0.1mm' },
1282
+ 'sheet_metal': { precision: 0.5, grade: 'IT14', typical: '±0.5mm' }
1283
+ };
1284
+
1285
+ const analysis = tolerances.map(tol => {
1286
+ const achievable = [];
1287
+ const notAchievable = [];
1288
+
1289
+ for (const proc of processes) {
1290
+ const cap = capabilities[proc];
1291
+ if (tol.tolerance <= cap.precision) {
1292
+ achievable.push({ process: proc, precision: cap.precision, grade: cap.grade });
1293
+ } else {
1294
+ notAchievable.push({ process: proc, required: tol.tolerance, achievable: cap.precision });
1295
+ }
1296
+ }
1297
+
1298
+ return {
1299
+ feature: tol.feature,
1300
+ tolerance: tol.tolerance,
1301
+ achievable: achievable.length > 0 ? achievable : null,
1302
+ notAchievable: notAchievable.length > 0 ? notAchievable : null,
1303
+ costImpact: tol.tolerance < 0.1 ? 'high' : tol.tolerance < 0.3 ? 'medium' : 'low'
1304
+ };
1305
+ });
1306
+
1307
+ return { ok: true, tolerances: analysis, timestamp: Date.now() };
1308
+ }
1309
+
1310
+ /**
1311
+ * Generate full DFM report as HTML
1312
+ */
1313
+ function generateReport(mesh, options = {}) {
1314
+ const title = options.title || 'DFM Analysis Report';
1315
+ const process = options.process || 'fdm';
1316
+ const material = options.material || 'steel-1018';
1317
+ const quantity = options.quantity || 1;
1318
+
1319
+ const dfmResult = analyze(mesh, process);
1320
+ const costResult = estimateCost(mesh, process, quantity, material);
1321
+ const allResults = analyzeAll(mesh);
1322
+
1323
+ const html = `
1324
+ <!DOCTYPE html>
1325
+ <html lang="en">
1326
+ <head>
1327
+ <meta charset="UTF-8">
1328
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1329
+ <title>${title}</title>
1330
+ <style>
1331
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1332
+ body {
1333
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1334
+ background: #f5f5f5;
1335
+ padding: 40px;
1336
+ color: #333;
1337
+ }
1338
+ .container { max-width: 900px; margin: 0 auto; background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
1339
+ h1 { font-size: 28px; margin-bottom: 10px; color: #000; }
1340
+ .subtitle { color: #666; margin-bottom: 30px; }
1341
+ h2 { font-size: 20px; margin-top: 30px; margin-bottom: 15px; color: #000; border-bottom: 2px solid #059669; padding-bottom: 10px; }
1342
+ h3 { font-size: 16px; margin-top: 15px; margin-bottom: 10px; color: #111; }
1343
+
1344
+ .grade-card {
1345
+ display: inline-block;
1346
+ padding: 20px 30px;
1347
+ background: linear-gradient(135deg, #059669 0%, #047857 100%);
1348
+ color: white;
1349
+ border-radius: 8px;
1350
+ margin-bottom: 20px;
1351
+ font-size: 24px;
1352
+ font-weight: bold;
1353
+ }
1354
+ .score { font-size: 14px; margin-top: 5px; }
1355
+
1356
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
1357
+ .card {
1358
+ padding: 15px;
1359
+ background: #f9fafb;
1360
+ border-left: 4px solid #059669;
1361
+ border-radius: 4px;
1362
+ }
1363
+ .card strong { color: #000; }
1364
+ .card.warn { border-left-color: #f59e0b; }
1365
+ .card.fail { border-left-color: #ef4444; }
1366
+ .card.info { border-left-color: #3b82f6; }
1367
+
1368
+ table {
1369
+ width: 100%;
1370
+ border-collapse: collapse;
1371
+ margin: 15px 0;
1372
+ }
1373
+ th, td {
1374
+ padding: 10px;
1375
+ text-align: left;
1376
+ border-bottom: 1px solid #e5e7eb;
1377
+ }
1378
+ th { background: #f3f4f6; font-weight: 600; }
1379
+
1380
+ .process-grid {
1381
+ display: grid;
1382
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1383
+ gap: 15px;
1384
+ margin: 20px 0;
1385
+ }
1386
+ .process-item {
1387
+ padding: 15px;
1388
+ background: #f9fafb;
1389
+ border-radius: 6px;
1390
+ text-align: center;
1391
+ border: 2px solid transparent;
1392
+ }
1393
+ .process-item.best { border-color: #059669; background: #ecfdf5; }
1394
+ .grade { font-size: 24px; font-weight: bold; color: #059669; margin: 10px 0; }
1395
+
1396
+ .issues, .warnings, .suggestions {
1397
+ margin-bottom: 20px;
1398
+ }
1399
+ .issue, .warning, .suggestion {
1400
+ padding: 10px 15px;
1401
+ margin-bottom: 8px;
1402
+ border-radius: 4px;
1403
+ }
1404
+ .issue { background: #fee2e2; color: #991b1b; border-left: 4px solid #ef4444; }
1405
+ .warning { background: #fef3c7; color: #92400e; border-left: 4px solid #f59e0b; }
1406
+ .suggestion { background: #dbeafe; color: #0c2d6b; border-left: 4px solid #3b82f6; }
1407
+
1408
+ .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #666; }
1409
+ </style>
1410
+ </head>
1411
+ <body>
1412
+ <div class="container">
1413
+ <h1>${title}</h1>
1414
+ <p class="subtitle">Design for Manufacturability (DFM) Analysis Report</p>
1415
+
1416
+ <div class="grade-card">
1417
+ ${dfmResult.grade}
1418
+ <div class="score">${dfmResult.score}/100 — ${dfmResult.processName}</div>
1419
+ </div>
1420
+
1421
+ <h2>Process Analysis: ${dfmResult.processName}</h2>
1422
+ ${dfmResult.issues.length > 0 ? `
1423
+ <div class="issues">
1424
+ <h3 style="color: #ef4444;">Issues (${dfmResult.issues.length})</h3>
1425
+ ${dfmResult.issues.map(i => `<div class="issue"><strong>${i.name}:</strong> ${i.message}</div>`).join('')}
1426
+ </div>
1427
+ ` : ''}
1428
+
1429
+ ${dfmResult.warnings.length > 0 ? `
1430
+ <div class="warnings">
1431
+ <h3 style="color: #f59e0b;">Warnings (${dfmResult.warnings.length})</h3>
1432
+ ${dfmResult.warnings.map(w => `<div class="warning"><strong>${w.name}:</strong> ${w.message}</div>`).join('')}
1433
+ </div>
1434
+ ` : ''}
1435
+
1436
+ ${dfmResult.suggestions.length > 0 ? `
1437
+ <div class="suggestions">
1438
+ <h3 style="color: #3b82f6;">Suggestions</h3>
1439
+ ${dfmResult.suggestions.map(s => `<div class="suggestion">${s}</div>`).join('')}
1440
+ </div>
1441
+ ` : ''}
1442
+
1443
+ <h2>Cost Estimation</h2>
1444
+ <div class="grid">
1445
+ <div class="card">
1446
+ <strong>Material Cost (per unit)</strong><br>
1447
+ €${costResult.materialCost.perUnit}
1448
+ </div>
1449
+ <div class="card">
1450
+ <strong>Machine Cost (per unit)</strong><br>
1451
+ €${costResult.machineCost.total / quantity}
1452
+ </div>
1453
+ <div class="card">
1454
+ <strong>Total Cost (per unit)</strong><br>
1455
+ €${costResult.totalPerUnit}
1456
+ </div>
1457
+ <div class="card">
1458
+ <strong>Total Cost (${quantity} units)</strong><br>
1459
+ €${costResult.totalBatch}
1460
+ </div>
1461
+ </div>
1462
+
1463
+ <h2>Process Comparison</h2>
1464
+ <div class="process-grid">
1465
+ ${Object.entries(allResults.processes).map(([k, v]) => `
1466
+ <div class="process-item ${k === allResults.bestProcess ? 'best' : ''}">
1467
+ <div>${DFM_RULES[k].name}</div>
1468
+ <div class="grade">${v.grade}</div>
1469
+ <div style="font-size: 12px; color: #666;">${v.score}/100</div>
1470
+ </div>
1471
+ `).join('')}
1472
+ </div>
1473
+
1474
+ <h2>Detailed Breakdown</h2>
1475
+ <h3>Material: ${costResult.material.materialType}</h3>
1476
+ <table>
1477
+ <tr>
1478
+ <th>Component</th>
1479
+ <th>Per Unit</th>
1480
+ <th>Total (${quantity})</th>
1481
+ </tr>
1482
+ <tr>
1483
+ <td>Material</td>
1484
+ <td>€${costResult.materialCost.perUnit}</td>
1485
+ <td>€${costResult.materialCost.total}</td>
1486
+ </tr>
1487
+ <tr>
1488
+ <td>Machine Time</td>
1489
+ <td>€${costResult.machineCost.total / quantity}</td>
1490
+ <td>€${costResult.machineCost.total}</td>
1491
+ </tr>
1492
+ <tr>
1493
+ <td>Setup & Fixtures</td>
1494
+ <td>€${costResult.setupCost.amortized}</td>
1495
+ <td>€${parseFloat(costResult.setupCost.amortized) * quantity}</td>
1496
+ </tr>
1497
+ ${costResult.toolingCost.total > 0 ? `
1498
+ <tr>
1499
+ <td>Tooling</td>
1500
+ <td>€${costResult.toolingCost.total / quantity}</td>
1501
+ <td>€${costResult.toolingCost.total}</td>
1502
+ </tr>
1503
+ ` : ''}
1504
+ <tr style="font-weight: bold; background: #f3f4f6;">
1505
+ <td>TOTAL</td>
1506
+ <td>€${costResult.totalPerUnit}</td>
1507
+ <td>€${costResult.totalBatch}</td>
1508
+ </tr>
1509
+ </table>
1510
+
1511
+ <div class="footer">
1512
+ <p>Report generated: ${new Date().toISOString()}</p>
1513
+ <p>DFM Analysis powered by cycleCAD</p>
1514
+ </div>
1515
+ </div>
1516
+ </body>
1517
+ </html>
1518
+ `;
1519
+
1520
+ emitEvent('dfm-report-generated', { title, process, html });
1521
+ return { ok: true, html, title, timestamp: Date.now() };
1522
+ }
1523
+
1524
+ // ============================================================================
1525
+ // EVENT SYSTEM
1526
+ // ============================================================================
1527
+
1528
+ const eventListeners = {};
1529
+
1530
+ function on(eventName, callback) {
1531
+ if (!eventListeners[eventName]) eventListeners[eventName] = [];
1532
+ eventListeners[eventName].push(callback);
1533
+ return () => {
1534
+ eventListeners[eventName] = eventListeners[eventName].filter(c => c !== callback);
1535
+ };
1536
+ }
1537
+
1538
+ function off(eventName, callback) {
1539
+ if (!eventListeners[eventName]) return;
1540
+ eventListeners[eventName] = eventListeners[eventName].filter(c => c !== callback);
1541
+ }
1542
+
1543
+ function emitEvent(eventName, data) {
1544
+ if (!eventListeners[eventName]) return;
1545
+ eventListeners[eventName].forEach(cb => {
1546
+ try {
1547
+ cb(data);
1548
+ } catch (e) {
1549
+ console.error(`[DFM] Event handler error for ${eventName}:`, e);
1550
+ }
1551
+ });
1552
+ }
1553
+
1554
+ // ============================================================================
1555
+ // UI PANEL CREATION
1556
+ // ============================================================================
1557
+
1558
+ /**
1559
+ * Create the DFM Analysis UI panel
1560
+ */
1561
+ function createAnalysisPanel() {
1562
+ const panel = document.createElement('div');
1563
+ panel.id = 'dfm-analysis-panel';
1564
+ panel.innerHTML = `
1565
+ <div style="
1566
+ position: fixed;
1567
+ right: 0;
1568
+ top: 200px;
1569
+ width: 350px;
1570
+ max-height: 600px;
1571
+ background: white;
1572
+ border: 1px solid #e5e7eb;
1573
+ border-radius: 6px;
1574
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
1575
+ overflow-y: auto;
1576
+ z-index: 1000;
1577
+ padding: 20px;
1578
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1579
+ ">
1580
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
1581
+ <h3 style="margin: 0; font-size: 16px;">DFM Analysis</h3>
1582
+ <button onclick="this.closest('#dfm-analysis-panel').style.display='none'"
1583
+ style="background: none; border: none; font-size: 18px; cursor: pointer; color: #666;">×</button>
1584
+ </div>
1585
+
1586
+ <div style="margin-bottom: 15px;">
1587
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 14px;">Process</label>
1588
+ <select id="dfm-process-select" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 13px;">
1589
+ <option value="fdm">FDM (3D Printing)</option>
1590
+ <option value="sla">SLA (Resin)</option>
1591
+ <option value="sls">SLS (Powder)</option>
1592
+ <option value="cnc_mill">CNC Milling</option>
1593
+ <option value="cnc_lathe">CNC Lathe</option>
1594
+ <option value="laser_cut">Laser Cutting</option>
1595
+ <option value="injection_mold">Injection Molding</option>
1596
+ <option value="sheet_metal">Sheet Metal</option>
1597
+ </select>
1598
+ </div>
1599
+
1600
+ <div style="margin-bottom: 15px;">
1601
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 14px;">Material</label>
1602
+ <select id="dfm-material-select" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 13px;">
1603
+ <option value="steel-1018">Steel (1018)</option>
1604
+ <option value="aluminum-6061">Aluminum (6061)</option>
1605
+ <option value="pla">PLA</option>
1606
+ <option value="abs">ABS</option>
1607
+ <option value="stainless-steel-316">Stainless Steel (316)</option>
1608
+ </select>
1609
+ </div>
1610
+
1611
+ <div style="margin-bottom: 15px;">
1612
+ <label style="display: block; margin-bottom: 8px; font-weight: 600; font-size: 14px;">Quantity</label>
1613
+ <input id="dfm-quantity-input" type="number" value="1" min="1"
1614
+ style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 13px;" />
1615
+ </div>
1616
+
1617
+ <button id="dfm-analyze-btn" style="
1618
+ width: 100%;
1619
+ padding: 10px;
1620
+ background: #059669;
1621
+ color: white;
1622
+ border: none;
1623
+ border-radius: 4px;
1624
+ font-weight: 600;
1625
+ cursor: pointer;
1626
+ margin-bottom: 20px;
1627
+ ">Analyze</button>
1628
+
1629
+ <div id="dfm-results" style="display: none;">
1630
+ <div style="padding: 12px; background: #f0fdf4; border: 1px solid #86efac; border-radius: 4px; margin-bottom: 15px;">
1631
+ <div style="font-size: 28px; font-weight: bold; color: #059669;" id="dfm-grade">—</div>
1632
+ <div style="font-size: 12px; color: #666; margin-top: 4px;" id="dfm-score">Score: —</div>
1633
+ </div>
1634
+
1635
+ <div id="dfm-issues" style="display: none; margin-bottom: 12px;">
1636
+ <div style="font-size: 12px; font-weight: 600; color: #ef4444; margin-bottom: 6px;">Issues:</div>
1637
+ <div id="dfm-issues-list" style="font-size: 12px;"></div>
1638
+ </div>
1639
+
1640
+ <div id="dfm-warnings" style="display: none; margin-bottom: 12px;">
1641
+ <div style="font-size: 12px; font-weight: 600; color: #f59e0b; margin-bottom: 6px;">Warnings:</div>
1642
+ <div id="dfm-warnings-list" style="font-size: 12px;"></div>
1643
+ </div>
1644
+
1645
+ <div style="padding: 10px; background: #f3f4f6; border-radius: 4px; margin-bottom: 12px;">
1646
+ <div style="font-size: 11px; font-weight: 600; margin-bottom: 4px;">Cost Estimate</div>
1647
+ <div style="font-size: 13px; font-weight: bold; color: #059669;" id="dfm-cost-total">—</div>
1648
+ <div style="font-size: 11px; color: #666; margin-top: 4px;" id="dfm-cost-per-unit">—</div>
1649
+ </div>
1650
+
1651
+ <button id="dfm-export-btn" style="
1652
+ width: 100%;
1653
+ padding: 8px;
1654
+ background: #3b82f6;
1655
+ color: white;
1656
+ border: none;
1657
+ border-radius: 4px;
1658
+ font-weight: 600;
1659
+ cursor: pointer;
1660
+ font-size: 13px;
1661
+ ">Export Report (HTML)</button>
1662
+ </div>
1663
+ </div>
1664
+ `;
1665
+
1666
+ document.body.appendChild(panel);
1667
+
1668
+ // Wire up event handlers
1669
+ document.getElementById('dfm-analyze-btn').addEventListener('click', () => {
1670
+ const process = document.getElementById('dfm-process-select').value;
1671
+ const material = document.getElementById('dfm-material-select').value;
1672
+ const quantity = parseInt(document.getElementById('dfm-quantity-input').value) || 1;
1673
+
1674
+ // Get selected mesh (simplified: assumes window._selectedMesh exists)
1675
+ const mesh = window._selectedMesh || window.allParts?.[0]?.mesh;
1676
+ if (!mesh) {
1677
+ alert('No mesh selected. Select a part first.');
1678
+ return;
1679
+ }
1680
+
1681
+ // Run analysis
1682
+ const result = analyze(mesh, process);
1683
+ if (!result.ok) {
1684
+ alert('Analysis failed: ' + result.error);
1685
+ return;
1686
+ }
1687
+
1688
+ // Display results
1689
+ document.getElementById('dfm-results').style.display = 'block';
1690
+ document.getElementById('dfm-grade').textContent = result.grade;
1691
+ document.getElementById('dfm-score').textContent = `Score: ${result.score}/100 — ${result.processName}`;
1692
+
1693
+ if (result.issues.length > 0) {
1694
+ document.getElementById('dfm-issues').style.display = 'block';
1695
+ document.getElementById('dfm-issues-list').innerHTML = result.issues
1696
+ .map(i => `<div style="margin-bottom: 4px;">• ${i.name}</div>`)
1697
+ .join('');
1698
+ } else {
1699
+ document.getElementById('dfm-issues').style.display = 'none';
1700
+ }
1701
+
1702
+ if (result.warnings.length > 0) {
1703
+ document.getElementById('dfm-warnings').style.display = 'block';
1704
+ document.getElementById('dfm-warnings-list').innerHTML = result.warnings
1705
+ .map(w => `<div style="margin-bottom: 4px;">• ${w.name}</div>`)
1706
+ .join('');
1707
+ } else {
1708
+ document.getElementById('dfm-warnings').style.display = 'none';
1709
+ }
1710
+
1711
+ // Cost estimate
1712
+ const cost = estimateCost(mesh, process, quantity, material);
1713
+ document.getElementById('dfm-cost-total').textContent = `€${cost.totalBatch}`;
1714
+ document.getElementById('dfm-cost-per-unit').textContent = `€${cost.totalPerUnit} per unit`;
1715
+
1716
+ // Export button handler
1717
+ document.getElementById('dfm-export-btn').onclick = () => {
1718
+ const report = generateReport(mesh, { process, material, quantity, title: `DFM Report - ${result.processName}` });
1719
+ const blob = new Blob([report.html], { type: 'text/html' });
1720
+ const url = URL.createObjectURL(blob);
1721
+ const a = document.createElement('a');
1722
+ a.href = url;
1723
+ a.download = `dfm-report-${process}-${Date.now()}.html`;
1724
+ a.click();
1725
+ URL.revokeObjectURL(url);
1726
+ };
1727
+ });
1728
+
1729
+ return panel;
1730
+ }
1731
+
1732
+ // ============================================================================
1733
+ // EXPORT API
1734
+ // ============================================================================
1735
+
1736
+ // Expose as window.cycleCAD.dfm
1737
+ window.cycleCAD = window.cycleCAD || {};
1738
+ window.cycleCAD.dfm = {
1739
+ analyze,
1740
+ analyzeAll,
1741
+ estimateCost,
1742
+ compareCosts,
1743
+ recommendMaterial,
1744
+ estimateWeight,
1745
+ analyzeTolerance,
1746
+ generateReport,
1747
+ createPanel: createAnalysisPanel,
1748
+ materials: MATERIALS,
1749
+ rules: DFM_RULES,
1750
+ on,
1751
+ off,
1752
+ // Internals for debugging
1753
+ _machineRates: MACHINE_RATES,
1754
+ _timeEstimates: TIME_ESTIMATES,
1755
+ _toolingCosts: TOOLING_COSTS
1756
+ };
1757
+
1758
+ console.log('[DFM Analyzer] Module loaded. Access via window.cycleCAD.dfm');
1759
+
1760
+ })();