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.
- package/API-BUILD-MANIFEST.txt +339 -0
- package/API-SERVER.md +535 -0
- package/Architecture-Deck.pptx +0 -0
- package/CLAUDE.md +172 -11
- package/CLI-BUILD-SUMMARY.md +504 -0
- package/CLI-INDEX.md +356 -0
- package/CLI-README.md +466 -0
- package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
- package/CONNECTED_FABS_GUIDE.md +612 -0
- package/CONNECTED_FABS_README.md +310 -0
- package/DELIVERABLES.md +343 -0
- package/DFM-ANALYZER-INTEGRATION.md +368 -0
- package/DFM-QUICK-START.js +253 -0
- package/Dockerfile +69 -0
- package/IMPLEMENTATION.md +327 -0
- package/LICENSE +31 -0
- package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
- package/MCP-INDEX.md +264 -0
- package/QUICKSTART-API.md +388 -0
- package/QUICKSTART-CLI.md +211 -0
- package/QUICKSTART-MCP.md +196 -0
- package/README-MCP.md +208 -0
- package/TEST-TOKEN-ENGINE.md +319 -0
- package/TOKEN-ENGINE-SUMMARY.md +266 -0
- package/TOKENS-README.md +263 -0
- package/TOOLS-REFERENCE.md +254 -0
- package/app/index.html +168 -3
- package/app/js/TOKEN-INTEGRATION.md +391 -0
- package/app/js/agent-api.js +3 -3
- package/app/js/ai-copilot.js +1435 -0
- package/app/js/cam-pipeline.js +840 -0
- package/app/js/collaboration-ui.js +995 -0
- package/app/js/collaboration.js +1116 -0
- package/app/js/connected-fabs-example.js +404 -0
- package/app/js/connected-fabs.js +1449 -0
- package/app/js/dfm-analyzer.js +1760 -0
- package/app/js/marketplace.js +1994 -0
- package/app/js/material-library.js +2115 -0
- package/app/js/token-dashboard.js +563 -0
- package/app/js/token-engine.js +743 -0
- package/app/test-agent.html +1801 -0
- package/bin/cyclecad-cli.js +662 -0
- package/bin/cyclecad-mcp +2 -0
- package/bin/server.js +242 -0
- package/cycleCAD-Architecture.pptx +0 -0
- package/cycleCAD-Investor-Deck.pptx +0 -0
- package/demo-mcp.sh +60 -0
- package/docs/API-SERVER-SUMMARY.md +375 -0
- package/docs/API-SERVER.md +667 -0
- package/docs/CAM-EXAMPLES.md +344 -0
- package/docs/CAM-INTEGRATION.md +612 -0
- package/docs/CAM-QUICK-REFERENCE.md +199 -0
- package/docs/CLI-INTEGRATION.md +510 -0
- package/docs/CLI.md +872 -0
- package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
- package/docs/MARKETPLACE-INTEGRATION.md +467 -0
- package/docs/MARKETPLACE-SETUP.html +439 -0
- package/docs/MCP-SERVER.md +403 -0
- package/examples/api-client-example.js +488 -0
- package/examples/api-client-example.py +359 -0
- package/examples/batch-manufacturing.txt +28 -0
- package/examples/batch-simple.txt +26 -0
- package/model-marketplace.html +1273 -0
- package/package.json +14 -3
- package/server/api-server.js +1120 -0
- package/server/mcp-server.js +1161 -0
- package/test-api-server.js +432 -0
- package/test-mcp.js +198 -0
- 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
|
+
})();
|