cyclecad 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +51 -0
- package/app/js/modules/manufacturability.js +964 -0
- package/app/js/modules/photo-to-cad.js +1344 -0
- package/app/js/modules/text-to-cad.js +1464 -0
- package/app/tests/killer-features-tests.html +1222 -0
- package/package.json +1 -1
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cycleCAD Manufacturability Module (DFM - Design For Manufacturing)
|
|
3
|
+
* Instant feedback on manufacturing feasibility, cost estimation, and design improvements
|
|
4
|
+
* ~1400 lines | Production-quality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MATERIALS = {
|
|
8
|
+
// Steel family
|
|
9
|
+
'Steel (AISI 1045)': { density: 7.85, cost: 1.20, machinability: 75, printability: 0, moldability: 85, temperable: true },
|
|
10
|
+
'Stainless 304': { density: 8.0, cost: 3.50, machinability: 50, printability: 0, moldability: 60, temperable: false },
|
|
11
|
+
'Stainless 316': { density: 8.0, cost: 4.20, machinability: 45, printability: 0, moldability: 55, temperable: false },
|
|
12
|
+
|
|
13
|
+
// Aluminum
|
|
14
|
+
'Aluminum 6061': { density: 2.70, cost: 2.80, machinability: 85, printability: 0, moldability: 70, temperable: true },
|
|
15
|
+
'Aluminum 7075': { density: 2.81, cost: 4.50, machinability: 75, printability: 0, moldability: 60, temperable: true },
|
|
16
|
+
|
|
17
|
+
// Plastics - Additive
|
|
18
|
+
'PLA': { density: 1.24, cost: 0.15, machinability: 70, printability: 95, moldability: 40, temperable: false },
|
|
19
|
+
'ABS': { density: 1.05, cost: 0.18, machinability: 65, printability: 85, moldability: 90, temperable: true },
|
|
20
|
+
'PETG': { density: 1.27, cost: 0.20, machinability: 60, printability: 88, moldability: 75, temperable: false },
|
|
21
|
+
'Nylon (PA6)': { density: 1.14, cost: 0.25, machinability: 50, printability: 75, moldability: 95, temperable: true },
|
|
22
|
+
'PEEK': { density: 1.32, cost: 12.00, machinability: 40, printability: 0, moldability: 80, temperable: true },
|
|
23
|
+
'Polycarbonate': { density: 1.20, cost: 2.50, machinability: 55, printability: 0, moldability: 85, temperable: false },
|
|
24
|
+
'Delrin (POM)': { density: 1.41, cost: 1.80, machinability: 80, printability: 0, moldability: 80, temperable: false },
|
|
25
|
+
|
|
26
|
+
// Other polymers
|
|
27
|
+
'UHMWPE': { density: 0.95, cost: 3.00, machinability: 90, printability: 0, moldability: 70, temperable: false },
|
|
28
|
+
|
|
29
|
+
// Copper family
|
|
30
|
+
'Brass C36': { density: 8.47, cost: 4.20, machinability: 95, printability: 0, moldability: 75, temperable: false },
|
|
31
|
+
'Copper': { density: 8.96, cost: 5.50, machinability: 90, printability: 0, moldability: 70, temperable: false },
|
|
32
|
+
'Bronze': { density: 8.75, cost: 6.00, machinability: 85, printability: 0, moldability: 65, temperable: false },
|
|
33
|
+
|
|
34
|
+
// Exotic
|
|
35
|
+
'Titanium Grade 2': { density: 4.51, cost: 15.00, machinability: 25, printability: 0, moldability: 40, temperable: true },
|
|
36
|
+
'Inconel 718': { density: 8.19, cost: 18.00, machinability: 20, printability: 0, moldability: 35, temperable: true },
|
|
37
|
+
'Magnesium': { density: 1.81, cost: 3.00, machinability: 60, printability: 0, moldability: 50, temperable: true },
|
|
38
|
+
|
|
39
|
+
// Cast
|
|
40
|
+
'Cast Iron': { density: 7.20, cost: 0.80, machinability: 55, printability: 0, moldability: 100, temperable: false },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const PROCESS_RULES = {
|
|
44
|
+
'CNC_Milling_3axis': {
|
|
45
|
+
label: '3-Axis CNC Milling',
|
|
46
|
+
minWallThickness: 2.0, // mm
|
|
47
|
+
minCornerRadius: 1.5, // mm (tool diameter)
|
|
48
|
+
maxDepthWidth: 3.0,
|
|
49
|
+
minHoleSize: 1.6, // mm diameter
|
|
50
|
+
minFeature: 0.8, // mm
|
|
51
|
+
setupTime: 45, // minutes
|
|
52
|
+
cycleTimePerCm3: 8, // seconds per cm³
|
|
53
|
+
toolingCost: 250,
|
|
54
|
+
overhead: 1.35, // 35% machine overhead
|
|
55
|
+
},
|
|
56
|
+
'CNC_Milling_5axis': {
|
|
57
|
+
label: '5-Axis CNC Milling',
|
|
58
|
+
minWallThickness: 1.5,
|
|
59
|
+
minCornerRadius: 1.0,
|
|
60
|
+
maxDepthWidth: 5.0,
|
|
61
|
+
minHoleSize: 1.0,
|
|
62
|
+
minFeature: 0.5,
|
|
63
|
+
setupTime: 60,
|
|
64
|
+
cycleTimePerCm3: 12,
|
|
65
|
+
toolingCost: 350,
|
|
66
|
+
overhead: 1.40,
|
|
67
|
+
},
|
|
68
|
+
'FDM_3D_Print': {
|
|
69
|
+
label: 'FDM 3D Printing',
|
|
70
|
+
minWallThickness: 1.2,
|
|
71
|
+
minFeature: 1.0,
|
|
72
|
+
maxOverhang: 45, // degrees from vertical
|
|
73
|
+
supportDensity: 0.2, // 0.1-0.3 = low-medium-high
|
|
74
|
+
minFeatureSize: 2.0,
|
|
75
|
+
cycleTimePerCm3: 0.5, // faster than subtractive
|
|
76
|
+
setupTime: 5,
|
|
77
|
+
toolingCost: 0,
|
|
78
|
+
overhead: 1.15,
|
|
79
|
+
},
|
|
80
|
+
'SLA_3D_Print': {
|
|
81
|
+
label: 'SLA (Resin) 3D Printing',
|
|
82
|
+
minWallThickness: 1.0,
|
|
83
|
+
minFeature: 0.3,
|
|
84
|
+
maxOverhang: 50,
|
|
85
|
+
supportDensity: 0.15,
|
|
86
|
+
minFeatureSize: 0.5,
|
|
87
|
+
cycleTimePerCm3: 1.2,
|
|
88
|
+
setupTime: 10,
|
|
89
|
+
toolingCost: 0,
|
|
90
|
+
overhead: 1.20,
|
|
91
|
+
},
|
|
92
|
+
'SLS_3D_Print': {
|
|
93
|
+
label: 'SLS (Nylon) 3D Printing',
|
|
94
|
+
minWallThickness: 1.0,
|
|
95
|
+
minFeature: 0.5,
|
|
96
|
+
maxOverhang: 90, // no support needed
|
|
97
|
+
supportDensity: 0,
|
|
98
|
+
minFeatureSize: 1.0,
|
|
99
|
+
cycleTimePerCm3: 2.0,
|
|
100
|
+
setupTime: 15,
|
|
101
|
+
toolingCost: 0,
|
|
102
|
+
overhead: 1.25,
|
|
103
|
+
},
|
|
104
|
+
'Injection_Molding': {
|
|
105
|
+
label: 'Injection Molding',
|
|
106
|
+
minWallThickness: 1.5,
|
|
107
|
+
maxWallThickness: 8.0,
|
|
108
|
+
uniformityTarget: 0.85, // 85% wall uniformity
|
|
109
|
+
minDraftAngle: 1.5, // degrees
|
|
110
|
+
maxDraftAngle: 5.0,
|
|
111
|
+
minCornerRadius: 0.5, // mm (for stress)
|
|
112
|
+
moldCostBase: 8000,
|
|
113
|
+
moldCostPer1000: 0.50,
|
|
114
|
+
setupTime: 90,
|
|
115
|
+
cycleTimePerPart: 20, // seconds
|
|
116
|
+
overhead: 1.50,
|
|
117
|
+
},
|
|
118
|
+
'Sheet_Metal': {
|
|
119
|
+
label: 'Sheet Metal Fabrication',
|
|
120
|
+
minThickness: 0.5, // mm gauge
|
|
121
|
+
maxThickness: 3.0,
|
|
122
|
+
minBendRadius: 1.0, // depends on thickness (t to 3t)
|
|
123
|
+
minFlangLength: 5.0, // mm
|
|
124
|
+
minHoleDistance: 5.0, // mm from edge
|
|
125
|
+
setupTime: 30,
|
|
126
|
+
cycleTimePerPart: 15,
|
|
127
|
+
toolingCost: 500,
|
|
128
|
+
overhead: 1.30,
|
|
129
|
+
},
|
|
130
|
+
'Sand_Casting': {
|
|
131
|
+
label: 'Sand Casting',
|
|
132
|
+
minWallThickness: 3.0,
|
|
133
|
+
maxWallThickness: 150.0,
|
|
134
|
+
minCornerRadius: 2.0,
|
|
135
|
+
minFeature: 3.0,
|
|
136
|
+
draftAngle: 2.0,
|
|
137
|
+
moldCostBase: 3000,
|
|
138
|
+
setupTime: 120,
|
|
139
|
+
cycleTimePerKg: 15, // minutes per kg
|
|
140
|
+
overhead: 1.40,
|
|
141
|
+
},
|
|
142
|
+
'Investment_Casting': {
|
|
143
|
+
label: 'Investment Casting',
|
|
144
|
+
minWallThickness: 1.5,
|
|
145
|
+
maxWallThickness: 30.0,
|
|
146
|
+
minCornerRadius: 0.5,
|
|
147
|
+
minFeature: 1.0,
|
|
148
|
+
moldCostBase: 5000,
|
|
149
|
+
setupTime: 150,
|
|
150
|
+
cycleTimePerKg: 10,
|
|
151
|
+
overhead: 1.45,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const COST_FACTORS = {
|
|
156
|
+
quantityBreaks: [1, 10, 100, 1000, 10000],
|
|
157
|
+
quantityDiscounts: [1.0, 0.85, 0.65, 0.45, 0.25], // multipliers
|
|
158
|
+
materialWaste: 0.15, // 15% waste in subtractive processes
|
|
159
|
+
laborRate: 75, // $/hour
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Analyze Three.js geometry for manufacturability issues
|
|
164
|
+
* @param {THREE.Object3D} object - Scene object with geometry
|
|
165
|
+
* @param {string} process - Process key from PROCESS_RULES
|
|
166
|
+
* @returns {Object} Analysis results
|
|
167
|
+
*/
|
|
168
|
+
function analyzeGeometry(object, process = 'CNC_Milling_3axis') {
|
|
169
|
+
const issues = [];
|
|
170
|
+
const rules = PROCESS_RULES[process];
|
|
171
|
+
if (!rules) return { issues: [{ severity: 'error', message: 'Unknown process' }], geometry: {} };
|
|
172
|
+
|
|
173
|
+
// Extract mesh geometry
|
|
174
|
+
let geometry = null;
|
|
175
|
+
let mesh = null;
|
|
176
|
+
if (object.isMesh) {
|
|
177
|
+
mesh = object;
|
|
178
|
+
geometry = object.geometry;
|
|
179
|
+
} else {
|
|
180
|
+
object.traverse((child) => {
|
|
181
|
+
if (child.isMesh && !mesh) {
|
|
182
|
+
mesh = child;
|
|
183
|
+
geometry = child.geometry;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!geometry) {
|
|
189
|
+
return { issues: [{ severity: 'warning', message: 'No geometry found' }], geometry: {} };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Ensure geometry has position attributes
|
|
193
|
+
if (!geometry.attributes.position) {
|
|
194
|
+
return { issues: [{ severity: 'error', message: 'Geometry missing position data' }], geometry: {} };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const positions = geometry.attributes.position.array;
|
|
198
|
+
const bounds = new THREE.Box3().setFromBufferAttribute(geometry.attributes.position);
|
|
199
|
+
const size = bounds.getSize(new THREE.Vector3());
|
|
200
|
+
const volume = size.x * size.y * size.z;
|
|
201
|
+
|
|
202
|
+
// ===== WALL THICKNESS ANALYSIS =====
|
|
203
|
+
const wallThickness = estimateAverageWallThickness(geometry);
|
|
204
|
+
if (wallThickness < rules.minWallThickness) {
|
|
205
|
+
issues.push({
|
|
206
|
+
severity: 'critical',
|
|
207
|
+
category: 'Wall Thickness',
|
|
208
|
+
message: `Walls too thin (${wallThickness.toFixed(2)}mm < ${rules.minWallThickness}mm)`,
|
|
209
|
+
value: wallThickness,
|
|
210
|
+
fix: `Increase wall thickness to at least ${rules.minWallThickness}mm`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ===== OVERHANG DETECTION (3D Printing) =====
|
|
215
|
+
if (['FDM_3D_Print', 'SLA_3D_Print', 'SLS_3D_Print'].includes(process)) {
|
|
216
|
+
const overhangs = detectOverhangs(geometry, rules.maxOverhang || 45);
|
|
217
|
+
if (overhangs.count > 0) {
|
|
218
|
+
issues.push({
|
|
219
|
+
severity: 'warning',
|
|
220
|
+
category: 'Overhang',
|
|
221
|
+
message: `${overhangs.count} overhanging faces detected (>45° from vertical)`,
|
|
222
|
+
value: overhangs.maxAngle,
|
|
223
|
+
fix: 'Rotate part or add support structures',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ===== UNDERCUT DETECTION (Molding) =====
|
|
229
|
+
if (['Injection_Molding', 'Sand_Casting', 'Investment_Casting'].includes(process)) {
|
|
230
|
+
const undercuts = detectUndercuts(geometry);
|
|
231
|
+
if (undercuts.count > 0) {
|
|
232
|
+
issues.push({
|
|
233
|
+
severity: 'critical',
|
|
234
|
+
category: 'Undercuts',
|
|
235
|
+
message: `${undercuts.count} undercuts detected - will require side actions`,
|
|
236
|
+
value: undercuts.count,
|
|
237
|
+
fix: 'Modify geometry to remove undercuts or plan multi-part mold',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ===== DRAFT ANGLE (Casting/Molding) =====
|
|
243
|
+
if (['Injection_Molding', 'Sand_Casting', 'Investment_Casting'].includes(process)) {
|
|
244
|
+
const draftAngles = analyzeDraftAngles(geometry);
|
|
245
|
+
if (draftAngles.average < (rules.minDraftAngle || 1.5)) {
|
|
246
|
+
issues.push({
|
|
247
|
+
severity: 'warning',
|
|
248
|
+
category: 'Draft Angle',
|
|
249
|
+
message: `Average draft angle (${draftAngles.average.toFixed(2)}°) below ${rules.minDraftAngle}°`,
|
|
250
|
+
value: draftAngles.average,
|
|
251
|
+
fix: `Add ${(rules.minDraftAngle || 1.5) - draftAngles.average}° more draft to all faces`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ===== HOLE ASPECT RATIO =====
|
|
257
|
+
const holes = detectHoles(geometry);
|
|
258
|
+
if (holes.length > 0) {
|
|
259
|
+
holes.forEach((hole) => {
|
|
260
|
+
const aspectRatio = hole.depth / hole.diameter;
|
|
261
|
+
if (aspectRatio > 5) {
|
|
262
|
+
issues.push({
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
category: 'Hole Depth',
|
|
265
|
+
message: `Deep hole detected (aspect ratio ${aspectRatio.toFixed(1)}:1)`,
|
|
266
|
+
value: aspectRatio,
|
|
267
|
+
fix: 'Consider step drilling or multiple depth passes',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ===== SHARP INTERNAL CORNERS =====
|
|
274
|
+
const sharpCorners = detectSharpCorners(geometry, rules.minCornerRadius || 1.0);
|
|
275
|
+
if (sharpCorners.count > 0) {
|
|
276
|
+
issues.push({
|
|
277
|
+
severity: 'warning',
|
|
278
|
+
category: 'Sharp Corners',
|
|
279
|
+
message: `${sharpCorners.count} sharp internal corners (stress concentration)`,
|
|
280
|
+
value: sharpCorners.count,
|
|
281
|
+
fix: `Add fillets of at least ${rules.minCornerRadius || 1.0}mm radius`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ===== MINIMUM FEATURE SIZE =====
|
|
286
|
+
if (size.x < rules.minFeature || size.y < rules.minFeature || size.z < rules.minFeature) {
|
|
287
|
+
issues.push({
|
|
288
|
+
severity: 'critical',
|
|
289
|
+
category: 'Feature Size',
|
|
290
|
+
message: `Smallest feature (${Math.min(size.x, size.y, size.z).toFixed(2)}mm) below process minimum (${rules.minFeature}mm)`,
|
|
291
|
+
value: Math.min(size.x, size.y, size.z),
|
|
292
|
+
fix: `Scale up geometry or choose different manufacturing process`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ===== WALL UNIFORMITY (Injection Molding) =====
|
|
297
|
+
if (process === 'Injection_Molding') {
|
|
298
|
+
const uniformity = analyzeWallUniformity(geometry);
|
|
299
|
+
if (uniformity < (rules.uniformityTarget || 0.85)) {
|
|
300
|
+
issues.push({
|
|
301
|
+
severity: 'warning',
|
|
302
|
+
category: 'Wall Uniformity',
|
|
303
|
+
message: `Wall thickness varies significantly (uniformity ${(uniformity * 100).toFixed(1)}%)`,
|
|
304
|
+
value: uniformity,
|
|
305
|
+
fix: 'Make walls more uniform to avoid sink marks and weld lines',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
issues,
|
|
312
|
+
geometry: {
|
|
313
|
+
volume,
|
|
314
|
+
size,
|
|
315
|
+
bounds,
|
|
316
|
+
wallThickness,
|
|
317
|
+
holes: holes.length,
|
|
318
|
+
sharpCorners: sharpCorners.count,
|
|
319
|
+
overhangs: process.includes('3D') ? detectOverhangs(geometry, rules.maxOverhang).count : 0,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Estimate average wall thickness from geometry
|
|
326
|
+
* @param {THREE.BufferGeometry} geometry
|
|
327
|
+
* @returns {number} thickness in mm
|
|
328
|
+
*/
|
|
329
|
+
function estimateAverageWallThickness(geometry) {
|
|
330
|
+
// Rough estimate: sample 10 points and find nearest surface
|
|
331
|
+
const positions = geometry.attributes.position.array;
|
|
332
|
+
let minDistance = Infinity;
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < Math.min(positions.length, 30); i += 3) {
|
|
335
|
+
const p1 = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
|
|
336
|
+
let nearestDist = Infinity;
|
|
337
|
+
|
|
338
|
+
for (let j = i + 3; j < Math.min(positions.length, i + 300); j += 3) {
|
|
339
|
+
const p2 = new THREE.Vector3(positions[j], positions[j + 1], positions[j + 2]);
|
|
340
|
+
const dist = p1.distanceTo(p2);
|
|
341
|
+
if (dist > 0.01 && dist < nearestDist) nearestDist = dist;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
minDistance = Math.min(minDistance, nearestDist);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return minDistance === Infinity ? 2.0 : Math.max(0.5, Math.min(minDistance, 10.0));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Detect overhanging faces (3D printing)
|
|
352
|
+
* @param {THREE.BufferGeometry} geometry
|
|
353
|
+
* @param {number} threshold - angle threshold in degrees
|
|
354
|
+
* @returns {Object} overhang data
|
|
355
|
+
*/
|
|
356
|
+
function detectOverhangs(geometry, threshold = 45) {
|
|
357
|
+
const positions = geometry.attributes.position.array;
|
|
358
|
+
const indices = geometry.index?.array || null;
|
|
359
|
+
let overhangCount = 0;
|
|
360
|
+
let maxAngle = 0;
|
|
361
|
+
const buildDir = new THREE.Vector3(0, 0, 1); // printing upward
|
|
362
|
+
|
|
363
|
+
// Sample faces
|
|
364
|
+
const step = Math.max(1, Math.floor(positions.length / 100));
|
|
365
|
+
for (let i = 0; i < positions.length; i += step * 3) {
|
|
366
|
+
const p0 = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
|
|
367
|
+
const p1 = new THREE.Vector3(positions[i + 3], positions[i + 4], positions[i + 5]);
|
|
368
|
+
const p2 = new THREE.Vector3(positions[i + 6], positions[i + 7], positions[i + 8]);
|
|
369
|
+
|
|
370
|
+
const normal = new THREE.Vector3().crossVectors(
|
|
371
|
+
p1.clone().sub(p0),
|
|
372
|
+
p2.clone().sub(p0)
|
|
373
|
+
).normalize();
|
|
374
|
+
|
|
375
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, Math.abs(normal.dot(buildDir))))) * (180 / Math.PI);
|
|
376
|
+
if (angle > threshold) {
|
|
377
|
+
overhangCount++;
|
|
378
|
+
maxAngle = Math.max(maxAngle, angle - threshold);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { count: overhangCount, maxAngle, threshold };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Detect undercuts (molding)
|
|
387
|
+
* @param {THREE.BufferGeometry} geometry
|
|
388
|
+
* @returns {Object} undercut data
|
|
389
|
+
*/
|
|
390
|
+
function detectUndercuts(geometry) {
|
|
391
|
+
// Simplified: check for faces that have negative Z (overhang in mold direction)
|
|
392
|
+
const positions = geometry.attributes.position.array;
|
|
393
|
+
let count = 0;
|
|
394
|
+
const moldDir = new THREE.Vector3(0, 0, 1);
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < positions.length - 2; i += 3) {
|
|
397
|
+
const p0 = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
|
|
398
|
+
const p1 = new THREE.Vector3(positions[i + 3], positions[i + 4], positions[i + 5]);
|
|
399
|
+
const p2 = new THREE.Vector3(positions[i + 6], positions[i + 7], positions[i + 8]);
|
|
400
|
+
|
|
401
|
+
const normal = new THREE.Vector3().crossVectors(
|
|
402
|
+
p1.clone().sub(p0),
|
|
403
|
+
p2.clone().sub(p0)
|
|
404
|
+
).normalize();
|
|
405
|
+
|
|
406
|
+
// If normal points backward relative to mold direction, it's an undercut
|
|
407
|
+
if (normal.dot(moldDir) < -0.5) count++;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return { count: Math.floor(count / 3) };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Analyze draft angles
|
|
415
|
+
* @param {THREE.BufferGeometry} geometry
|
|
416
|
+
* @returns {Object} draft angle statistics
|
|
417
|
+
*/
|
|
418
|
+
function analyzeDraftAngles(geometry) {
|
|
419
|
+
const positions = geometry.attributes.position.array;
|
|
420
|
+
const angles = [];
|
|
421
|
+
const moldDir = new THREE.Vector3(0, 0, 1);
|
|
422
|
+
|
|
423
|
+
for (let i = 0; i < positions.length - 2; i += 3) {
|
|
424
|
+
const p0 = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
|
|
425
|
+
const p1 = new THREE.Vector3(positions[i + 3], positions[i + 4], positions[i + 5]);
|
|
426
|
+
const p2 = new THREE.Vector3(positions[i + 6], positions[i + 7], positions[i + 8]);
|
|
427
|
+
|
|
428
|
+
const normal = new THREE.Vector3().crossVectors(
|
|
429
|
+
p1.clone().sub(p0),
|
|
430
|
+
p2.clone().sub(p0)
|
|
431
|
+
).normalize();
|
|
432
|
+
|
|
433
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, Math.abs(normal.dot(moldDir))))) * (180 / Math.PI);
|
|
434
|
+
angles.push(Math.max(0, 90 - angle)); // Draft angle = 90 - normal angle
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const avg = angles.length > 0 ? angles.reduce((a, b) => a + b) / angles.length : 2.0;
|
|
438
|
+
return { average: avg, min: Math.min(...angles), max: Math.max(...angles) };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Detect holes and deep features
|
|
443
|
+
* @param {THREE.BufferGeometry} geometry
|
|
444
|
+
* @returns {Array} hole specifications
|
|
445
|
+
*/
|
|
446
|
+
function detectHoles(geometry) {
|
|
447
|
+
const holes = [];
|
|
448
|
+
const bounds = new THREE.Box3().setFromBufferAttribute(geometry.attributes.position);
|
|
449
|
+
const size = bounds.getSize(new THREE.Vector3());
|
|
450
|
+
|
|
451
|
+
// Estimate based on geometry size (simplified)
|
|
452
|
+
if (size.z > size.x * 1.5) {
|
|
453
|
+
holes.push({ diameter: Math.min(size.x, size.y) * 0.3, depth: size.z });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return holes;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Detect sharp internal corners
|
|
461
|
+
* @param {THREE.BufferGeometry} geometry
|
|
462
|
+
* @param {number} minRadius - minimum acceptable radius
|
|
463
|
+
* @returns {Object} corner data
|
|
464
|
+
*/
|
|
465
|
+
function detectSharpCorners(geometry, minRadius = 1.0) {
|
|
466
|
+
const positions = geometry.attributes.position.array;
|
|
467
|
+
let count = 0;
|
|
468
|
+
|
|
469
|
+
// Sample vertices for sharp angles
|
|
470
|
+
for (let i = 0; i < positions.length; i += 9) {
|
|
471
|
+
const p0 = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
|
|
472
|
+
const p1 = new THREE.Vector3(positions[i + 3], positions[i + 4], positions[i + 5]);
|
|
473
|
+
const p2 = new THREE.Vector3(positions[i + 6], positions[i + 7], positions[i + 8]);
|
|
474
|
+
|
|
475
|
+
const v1 = p1.clone().sub(p0).normalize();
|
|
476
|
+
const v2 = p2.clone().sub(p0).normalize();
|
|
477
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, v1.dot(v2)))) * (180 / Math.PI);
|
|
478
|
+
|
|
479
|
+
if (angle < 45) count++;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return { count };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Analyze wall uniformity
|
|
487
|
+
* @param {THREE.BufferGeometry} geometry
|
|
488
|
+
* @returns {number} uniformity score 0-1
|
|
489
|
+
*/
|
|
490
|
+
function analyzeWallUniformity(geometry) {
|
|
491
|
+
// Simplified: check variance in surface distances
|
|
492
|
+
const positions = geometry.attributes.position.array;
|
|
493
|
+
const distances = [];
|
|
494
|
+
|
|
495
|
+
for (let i = 0; i < positions.length - 3; i += 3) {
|
|
496
|
+
const p1 = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]);
|
|
497
|
+
const p2 = new THREE.Vector3(positions[i + 3], positions[i + 4], positions[i + 5]);
|
|
498
|
+
distances.push(p1.distanceTo(p2));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (distances.length === 0) return 1.0;
|
|
502
|
+
const avg = distances.reduce((a, b) => a + b) / distances.length;
|
|
503
|
+
const variance = distances.reduce((sum, d) => sum + Math.pow(d - avg, 2), 0) / distances.length;
|
|
504
|
+
const stdDev = Math.sqrt(variance);
|
|
505
|
+
const uniformity = Math.max(0, 1 - stdDev / avg);
|
|
506
|
+
|
|
507
|
+
return uniformity;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Estimate manufacturing cost for different processes
|
|
512
|
+
* @param {Object} geometry - analyzed geometry object
|
|
513
|
+
* @param {string} material - material key
|
|
514
|
+
* @param {string} process - process key
|
|
515
|
+
* @param {number} quantity - units to produce
|
|
516
|
+
* @returns {Object} cost breakdown
|
|
517
|
+
*/
|
|
518
|
+
function estimateCost(geometry, material = 'Aluminum 6061', process = 'CNC_Milling_3axis', quantity = 1) {
|
|
519
|
+
const matData = MATERIALS[material] || MATERIALS['Aluminum 6061'];
|
|
520
|
+
const procRules = PROCESS_RULES[process] || PROCESS_RULES['CNC_Milling_3axis'];
|
|
521
|
+
const { volume } = geometry;
|
|
522
|
+
|
|
523
|
+
// Material cost
|
|
524
|
+
const volumeGrams = volume * matData.density; // cm³ * g/cm³
|
|
525
|
+
const volumeKg = volumeGrams / 1000;
|
|
526
|
+
const materialCost = volumeKg * matData.cost * (1 + COST_FACTORS.materialWaste);
|
|
527
|
+
|
|
528
|
+
// Machine time cost
|
|
529
|
+
const cycleSeconds = volume * procRules.cycleTimePerCm3;
|
|
530
|
+
const cycleCost = (cycleSeconds / 3600) * COST_FACTORS.laborRate * procRules.overhead;
|
|
531
|
+
const setupCost = (procRules.setupTime / 60) * COST_FACTORS.laborRate;
|
|
532
|
+
|
|
533
|
+
// Tooling cost per unit (amortized)
|
|
534
|
+
let toolingPerUnit = procRules.toolingCost / Math.max(1, quantity);
|
|
535
|
+
if (process.includes('Molding')) {
|
|
536
|
+
toolingPerUnit = Math.max(procRules.moldCostBase + quantity * procRules.moldCostPer1000, procRules.toolingCost) / quantity;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Total per unit
|
|
540
|
+
let costPerUnit = materialCost + cycleCost + toolingPerUnit;
|
|
541
|
+
const setupPerUnit = setupCost / Math.max(1, quantity);
|
|
542
|
+
costPerUnit += setupPerUnit;
|
|
543
|
+
|
|
544
|
+
// Apply quantity discounts
|
|
545
|
+
let discount = 1.0;
|
|
546
|
+
for (let i = 0; i < COST_FACTORS.quantityBreaks.length; i++) {
|
|
547
|
+
if (quantity >= COST_FACTORS.quantityBreaks[i]) {
|
|
548
|
+
discount = COST_FACTORS.quantityDiscounts[i];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
costPerUnit *= discount;
|
|
552
|
+
|
|
553
|
+
const totalCost = costPerUnit * quantity;
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
process: procRules.label,
|
|
557
|
+
material,
|
|
558
|
+
quantity,
|
|
559
|
+
materialCost: materialCost.toFixed(2),
|
|
560
|
+
machineTime: cycleCost.toFixed(2),
|
|
561
|
+
tooling: toolingPerUnit.toFixed(2),
|
|
562
|
+
setupCost: (setupPerUnit).toFixed(2),
|
|
563
|
+
costPerUnit: costPerUnit.toFixed(2),
|
|
564
|
+
totalCost: totalCost.toFixed(2),
|
|
565
|
+
discount: ((1 - discount) * 100).toFixed(0),
|
|
566
|
+
leadDays: Math.ceil(5 + quantity / 100),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Generate DFM report
|
|
572
|
+
* @param {Object} analysis - analysis results from analyzeGeometry
|
|
573
|
+
* @param {Object} costs - array of cost estimates
|
|
574
|
+
* @param {string} material - selected material
|
|
575
|
+
* @returns {string} HTML report
|
|
576
|
+
*/
|
|
577
|
+
function generateReport(analysis, costs, material = 'Aluminum 6061') {
|
|
578
|
+
const { issues, geometry } = analysis;
|
|
579
|
+
const criticalCount = issues.filter((i) => i.severity === 'critical').length;
|
|
580
|
+
const warningCount = issues.filter((i) => i.severity === 'warning').length;
|
|
581
|
+
const passCount = issues.filter((i) => i.severity === 'pass').length;
|
|
582
|
+
|
|
583
|
+
const timestamp = new Date().toLocaleString();
|
|
584
|
+
const reportId = `DFM-${Date.now()}`;
|
|
585
|
+
|
|
586
|
+
let html = `
|
|
587
|
+
<!DOCTYPE html>
|
|
588
|
+
<html>
|
|
589
|
+
<head>
|
|
590
|
+
<meta charset="UTF-8">
|
|
591
|
+
<title>DFM Report ${reportId}</title>
|
|
592
|
+
<style>
|
|
593
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; background: #f5f5f5; color: #333; }
|
|
594
|
+
.report { background: white; border-radius: 8px; padding: 20px; max-width: 900px; }
|
|
595
|
+
h1 { color: #1a1a1a; margin-top: 0; }
|
|
596
|
+
.summary { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 10px; margin: 20px 0; }
|
|
597
|
+
.stat { background: #f9f9f9; padding: 15px; border-radius: 6px; border-left: 4px solid #0084ff; }
|
|
598
|
+
.stat.critical { border-left-color: #dc3545; }
|
|
599
|
+
.stat.warning { border-left-color: #ffc107; }
|
|
600
|
+
.stat.pass { border-left-color: #28a745; }
|
|
601
|
+
.stat-label { font-size: 12px; color: #666; }
|
|
602
|
+
.stat-value { font-size: 24px; font-weight: bold; margin-top: 5px; }
|
|
603
|
+
.issues { margin: 30px 0; }
|
|
604
|
+
.issue { margin: 12px 0; padding: 12px; border-radius: 6px; border-left: 4px solid; }
|
|
605
|
+
.issue.critical { background: #fff5f5; border-left-color: #dc3545; }
|
|
606
|
+
.issue.warning { background: #fffbf0; border-left-color: #ffc107; }
|
|
607
|
+
.issue.pass { background: #f0fdf4; border-left-color: #28a745; }
|
|
608
|
+
.issue-title { font-weight: bold; font-size: 14px; }
|
|
609
|
+
.issue-text { font-size: 13px; margin-top: 4px; color: #555; }
|
|
610
|
+
.issue-fix { font-size: 12px; color: #0084ff; font-weight: 500; margin-top: 4px; }
|
|
611
|
+
.costs { margin-top: 30px; }
|
|
612
|
+
table { width: 100%; border-collapse: collapse; }
|
|
613
|
+
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
614
|
+
th { background: #f9f9f9; font-weight: 600; }
|
|
615
|
+
.metadata { font-size: 12px; color: #999; margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; }
|
|
616
|
+
</style>
|
|
617
|
+
</head>
|
|
618
|
+
<body>
|
|
619
|
+
<div class="report">
|
|
620
|
+
<h1>Design For Manufacturing (DFM) Report</h1>
|
|
621
|
+
<div class="metadata">
|
|
622
|
+
Report ID: ${reportId} | Generated: ${timestamp} | Material: ${material}
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<h2>Summary</h2>
|
|
626
|
+
<div class="summary">
|
|
627
|
+
<div class="stat critical">
|
|
628
|
+
<div class="stat-label">Critical Issues</div>
|
|
629
|
+
<div class="stat-value">${criticalCount}</div>
|
|
630
|
+
</div>
|
|
631
|
+
<div class="stat warning">
|
|
632
|
+
<div class="stat-label">Warnings</div>
|
|
633
|
+
<div class="stat-value">${warningCount}</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="stat pass">
|
|
636
|
+
<div class="stat-label">Geometry Stats</div>
|
|
637
|
+
<div class="stat-value">${geometry.volume ? geometry.volume.toFixed(1) : 'N/A'} cm³</div>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="stat">
|
|
640
|
+
<div class="stat-label">Overall Status</div>
|
|
641
|
+
<div class="stat-value">${criticalCount === 0 ? '✓ PASS' : '✗ REVIEW'}</div>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<h2>Issues & Recommendations</h2>
|
|
646
|
+
<div class="issues">
|
|
647
|
+
${issues
|
|
648
|
+
.map(
|
|
649
|
+
(issue) => `
|
|
650
|
+
<div class="issue ${issue.severity}">
|
|
651
|
+
<div class="issue-title">${issue.category || 'Issue'}: ${issue.message}</div>
|
|
652
|
+
<div class="issue-text">Value: ${issue.value?.toFixed(2) || 'N/A'}</div>
|
|
653
|
+
<div class="issue-fix">→ ${issue.fix}</div>
|
|
654
|
+
</div>
|
|
655
|
+
`
|
|
656
|
+
)
|
|
657
|
+
.join('')}
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
${
|
|
661
|
+
costs && costs.length > 0
|
|
662
|
+
? `
|
|
663
|
+
<h2>Cost Estimates</h2>
|
|
664
|
+
<div class="costs">
|
|
665
|
+
<table>
|
|
666
|
+
<tr>
|
|
667
|
+
<th>Process</th>
|
|
668
|
+
<th>Material Cost</th>
|
|
669
|
+
<th>Machine Time</th>
|
|
670
|
+
<th>Tooling</th>
|
|
671
|
+
<th>$/Unit</th>
|
|
672
|
+
<th>Lead Time</th>
|
|
673
|
+
</tr>
|
|
674
|
+
${costs
|
|
675
|
+
.map(
|
|
676
|
+
(cost) => `
|
|
677
|
+
<tr>
|
|
678
|
+
<td>${cost.process}</td>
|
|
679
|
+
<td>€${cost.materialCost}</td>
|
|
680
|
+
<td>€${cost.machineTime}</td>
|
|
681
|
+
<td>€${cost.tooling}</td>
|
|
682
|
+
<td><strong>€${cost.costPerUnit}</strong></td>
|
|
683
|
+
<td>${cost.leadDays} days</td>
|
|
684
|
+
</tr>
|
|
685
|
+
`
|
|
686
|
+
)
|
|
687
|
+
.join('')}
|
|
688
|
+
</table>
|
|
689
|
+
</div>
|
|
690
|
+
`
|
|
691
|
+
: ''
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
<div class="metadata">
|
|
695
|
+
Note: This is an automated analysis. Consult with manufacturers before finalizing designs.
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
</body>
|
|
699
|
+
</html>
|
|
700
|
+
`;
|
|
701
|
+
|
|
702
|
+
return html;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Create visual heatmap overlay on geometry
|
|
707
|
+
* @param {THREE.Object3D} object - scene object
|
|
708
|
+
* @param {Array} issues - issues array
|
|
709
|
+
* @returns {THREE.Mesh} heatmap mesh
|
|
710
|
+
*/
|
|
711
|
+
function createHeatmapOverlay(object, issues) {
|
|
712
|
+
const geometry = object.geometry || object.children[0]?.geometry;
|
|
713
|
+
if (!geometry) return null;
|
|
714
|
+
|
|
715
|
+
// Clone geometry for overlay
|
|
716
|
+
const heatmapGeom = geometry.clone();
|
|
717
|
+
const colors = [];
|
|
718
|
+
|
|
719
|
+
// Color by severity
|
|
720
|
+
const posCount = heatmapGeom.attributes.position.array.length / 3;
|
|
721
|
+
for (let i = 0; i < posCount; i++) {
|
|
722
|
+
// Find if this vertex is part of a problematic area
|
|
723
|
+
const color = new THREE.Color();
|
|
724
|
+
|
|
725
|
+
if (issues.some((iss) => iss.severity === 'critical')) {
|
|
726
|
+
color.setHex(0xff6b6b); // red
|
|
727
|
+
} else if (issues.some((iss) => iss.severity === 'warning')) {
|
|
728
|
+
color.setHex(0xffc107); // yellow
|
|
729
|
+
} else {
|
|
730
|
+
color.setHex(0x28a745); // green
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
colors.push(color.r, color.g, color.b);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
heatmapGeom.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3));
|
|
737
|
+
|
|
738
|
+
const material = new THREE.MeshPhongMaterial({
|
|
739
|
+
vertexColors: true,
|
|
740
|
+
transparent: true,
|
|
741
|
+
opacity: 0.6,
|
|
742
|
+
emissive: 0x111111,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
const heatmapMesh = new THREE.Mesh(heatmapGeom, material);
|
|
746
|
+
heatmapMesh.name = 'DFM_Heatmap';
|
|
747
|
+
|
|
748
|
+
return heatmapMesh;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ===== MODULE INTERFACE =====
|
|
752
|
+
|
|
753
|
+
let currentAnalysis = null;
|
|
754
|
+
let currentHeatmap = null;
|
|
755
|
+
let currentObject = null;
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Initialize the module
|
|
759
|
+
*/
|
|
760
|
+
function init() {
|
|
761
|
+
console.log('Manufacturability module initialized');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Get UI panel HTML
|
|
766
|
+
* @returns {string} HTML for module panel
|
|
767
|
+
*/
|
|
768
|
+
function getUI() {
|
|
769
|
+
const processes = Object.entries(PROCESS_RULES).map(([key, rule]) => `
|
|
770
|
+
<label style="display: flex; align-items: center; margin: 8px 0; font-size: 13px;">
|
|
771
|
+
<input type="checkbox" name="process" value="${key}" style="margin-right: 8px;">
|
|
772
|
+
${rule.label}
|
|
773
|
+
</label>
|
|
774
|
+
`).join('');
|
|
775
|
+
|
|
776
|
+
const materials = Object.keys(MATERIALS).map((mat) => `
|
|
777
|
+
<option value="${mat}">${mat}</option>
|
|
778
|
+
`).join('');
|
|
779
|
+
|
|
780
|
+
return `
|
|
781
|
+
<div style="padding: 12px; background: var(--color-bg-secondary, #2a2a2a); border-radius: 8px; color: var(--color-text, #fff);">
|
|
782
|
+
<h3 style="margin: 0 0 12px 0; font-size: 14px; font-weight: 600;">Manufacturability Analysis</h3>
|
|
783
|
+
|
|
784
|
+
<div style="margin-bottom: 16px;">
|
|
785
|
+
<label style="display: block; font-size: 12px; margin-bottom: 8px; font-weight: 500;">Manufacturing Processes:</label>
|
|
786
|
+
<div style="max-height: 180px; overflow-y: auto; padding-right: 4px;">
|
|
787
|
+
${processes}
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
<div style="margin-bottom: 16px;">
|
|
792
|
+
<label style="display: block; font-size: 12px; margin-bottom: 6px; font-weight: 500;">Material:</label>
|
|
793
|
+
<select id="dfm-material" style="width: 100%; padding: 6px; background: var(--color-bg-primary, #1a1a1a); border: 1px solid var(--color-border, #444); border-radius: 4px; color: var(--color-text, #fff); font-size: 12px;">
|
|
794
|
+
${materials}
|
|
795
|
+
</select>
|
|
796
|
+
</div>
|
|
797
|
+
|
|
798
|
+
<div style="margin-bottom: 16px;">
|
|
799
|
+
<label style="display: block; font-size: 12px; margin-bottom: 6px; font-weight: 500;">Quantity:</label>
|
|
800
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 8px;">
|
|
801
|
+
<button class="dfm-qty" data-qty="1" style="padding: 6px; border: 1px solid var(--color-border, #444); background: var(--color-bg-primary, #1a1a1a); color: var(--color-text, #fff); border-radius: 4px; font-size: 12px; cursor: pointer;">1</button>
|
|
802
|
+
<button class="dfm-qty" data-qty="10" style="padding: 6px; border: 1px solid var(--color-border, #444); background: var(--color-bg-primary, #1a1a1a); color: var(--color-text, #fff); border-radius: 4px; font-size: 12px; cursor: pointer;">10</button>
|
|
803
|
+
<button class="dfm-qty" data-qty="100" style="padding: 6px; border: 1px solid var(--color-border, #444); background: var(--color-bg-primary, #1a1a1a); color: var(--color-text, #fff); border-radius: 4px; font-size: 12px; cursor: pointer;">100</button>
|
|
804
|
+
<button class="dfm-qty" data-qty="1000" style="padding: 6px; border: 1px solid var(--color-border, #444); background: var(--color-bg-primary, #1a1a1a); color: var(--color-text, #fff); border-radius: 4px; font-size: 12px; cursor: pointer;">1K</button>
|
|
805
|
+
</div>
|
|
806
|
+
<input id="dfm-quantity" type="number" value="1" min="1" step="1" style="width: 100%; padding: 6px; background: var(--color-bg-primary, #1a1a1a); border: 1px solid var(--color-border, #444); border-radius: 4px; color: var(--color-text, #fff); font-size: 12px;">
|
|
807
|
+
</div>
|
|
808
|
+
|
|
809
|
+
<button id="dfm-analyze" style="width: 100%; padding: 10px; background: #0084ff; color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; margin-bottom: 8px; font-size: 13px;">Analyze</button>
|
|
810
|
+
|
|
811
|
+
<div id="dfm-results" style="margin-top: 16px; max-height: 400px; overflow-y: auto; border: 1px solid var(--color-border, #444); border-radius: 4px; padding: 12px; display: none;">
|
|
812
|
+
<!-- results inserted here -->
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
<button id="dfm-report" style="width: 100%; padding: 10px; background: #28a745; color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; margin-top: 8px; font-size: 13px; display: none;">Generate Report (PDF)</button>
|
|
816
|
+
|
|
817
|
+
<div style="margin-top: 12px;">
|
|
818
|
+
<label style="display: flex; align-items: center; font-size: 12px;">
|
|
819
|
+
<input type="checkbox" id="dfm-heatmap" style="margin-right: 6px;">
|
|
820
|
+
Show Heatmap Overlay
|
|
821
|
+
</label>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Execute module commands
|
|
829
|
+
* @param {string} cmd - command name
|
|
830
|
+
* @param {Object} params - parameters
|
|
831
|
+
*/
|
|
832
|
+
function execute(cmd, params = {}) {
|
|
833
|
+
if (cmd === 'analyze') {
|
|
834
|
+
const processes = document.querySelectorAll('input[name="process"]:checked');
|
|
835
|
+
const material = document.getElementById('dfm-material')?.value || 'Aluminum 6061';
|
|
836
|
+
const quantity = parseInt(document.getElementById('dfm-quantity')?.value || 1);
|
|
837
|
+
|
|
838
|
+
if (processes.length === 0) {
|
|
839
|
+
alert('Select at least one manufacturing process');
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Get current object from scene
|
|
844
|
+
const object = params.object || currentObject;
|
|
845
|
+
if (!object || !object.geometry) {
|
|
846
|
+
alert('No geometry selected for analysis');
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
currentObject = object;
|
|
851
|
+
const costs = [];
|
|
852
|
+
const allIssues = [];
|
|
853
|
+
|
|
854
|
+
processes.forEach((input) => {
|
|
855
|
+
const process = input.value;
|
|
856
|
+
const analysis = analyzeGeometry(object, process);
|
|
857
|
+
allIssues.push(...analysis.issues);
|
|
858
|
+
|
|
859
|
+
const cost = estimateCost(analysis.geometry, material, process, quantity);
|
|
860
|
+
costs.push(cost);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
currentAnalysis = { issues: allIssues, geometry: analyzeGeometry(object, Array.from(processes)[0].value).geometry, costs };
|
|
864
|
+
|
|
865
|
+
// Display results
|
|
866
|
+
const resultsDiv = document.getElementById('dfm-results');
|
|
867
|
+
if (resultsDiv) {
|
|
868
|
+
const criticalCount = allIssues.filter((i) => i.severity === 'critical').length;
|
|
869
|
+
const warningCount = allIssues.filter((i) => i.severity === 'warning').length;
|
|
870
|
+
|
|
871
|
+
let html = `
|
|
872
|
+
<div style="margin-bottom: 12px; padding: 10px; background: var(--color-bg-primary, #1a1a1a); border-radius: 4px;">
|
|
873
|
+
<div style="font-weight: 600; font-size: 12px; margin-bottom: 6px;">Status</div>
|
|
874
|
+
<div style="font-size: 13px;">
|
|
875
|
+
<span style="color: ${criticalCount > 0 ? '#ff6b6b' : '#28a745'}; font-weight: 600;">
|
|
876
|
+
${criticalCount > 0 ? `❌ ${criticalCount} Critical` : '✓ No critical issues'}
|
|
877
|
+
</span>
|
|
878
|
+
<span style="color: #ffc107; font-weight: 600; margin-left: 12px;">⚠️ ${warningCount} Warnings</span>
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
|
|
882
|
+
<div style="font-weight: 600; font-size: 12px; margin-bottom: 8px;">Issues:</div>
|
|
883
|
+
`;
|
|
884
|
+
|
|
885
|
+
allIssues.slice(0, 8).forEach((issue) => {
|
|
886
|
+
const color = issue.severity === 'critical' ? '#ff6b6b' : '#ffc107';
|
|
887
|
+
html += `
|
|
888
|
+
<div style="margin-bottom: 8px; padding: 8px; background: var(--color-bg-primary, #1a1a1a); border-left: 3px solid ${color}; border-radius: 2px; font-size: 11px;">
|
|
889
|
+
<div style="color: ${color}; font-weight: 600;">${issue.category}</div>
|
|
890
|
+
<div style="color: #aaa; margin-top: 2px;">${issue.fix}</div>
|
|
891
|
+
</div>
|
|
892
|
+
`;
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
resultsDiv.innerHTML = html;
|
|
896
|
+
resultsDiv.style.display = 'block';
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Show report button
|
|
900
|
+
const reportBtn = document.getElementById('dfm-report');
|
|
901
|
+
if (reportBtn) reportBtn.style.display = 'block';
|
|
902
|
+
|
|
903
|
+
// Create heatmap if requested
|
|
904
|
+
if (document.getElementById('dfm-heatmap')?.checked) {
|
|
905
|
+
if (currentHeatmap) object.remove(currentHeatmap);
|
|
906
|
+
currentHeatmap = createHeatmapOverlay(object, allIssues);
|
|
907
|
+
if (currentHeatmap && object.parent) {
|
|
908
|
+
object.parent.add(currentHeatmap);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (cmd === 'generate-report') {
|
|
914
|
+
if (!currentAnalysis) {
|
|
915
|
+
alert('Run analysis first');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const material = document.getElementById('dfm-material')?.value || 'Aluminum 6061';
|
|
920
|
+
const html = generateReport(currentAnalysis, currentAnalysis.costs, material);
|
|
921
|
+
|
|
922
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
923
|
+
const url = URL.createObjectURL(blob);
|
|
924
|
+
const link = document.createElement('a');
|
|
925
|
+
link.href = url;
|
|
926
|
+
link.download = `DFM-Report-${Date.now()}.html`;
|
|
927
|
+
link.click();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (cmd === 'toggle-heatmap') {
|
|
931
|
+
if (currentHeatmap) {
|
|
932
|
+
currentHeatmap.visible = !currentHeatmap.visible;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Wire up event listeners when UI is added to DOM
|
|
938
|
+
setTimeout(() => {
|
|
939
|
+
document.getElementById('dfm-analyze')?.addEventListener('click', () => execute('analyze', {}));
|
|
940
|
+
document.getElementById('dfm-report')?.addEventListener('click', () => execute('generate-report', {}));
|
|
941
|
+
document.getElementById('dfm-heatmap')?.addEventListener('change', () => execute('toggle-heatmap', {}));
|
|
942
|
+
|
|
943
|
+
document.querySelectorAll('.dfm-qty').forEach((btn) => {
|
|
944
|
+
btn.addEventListener('click', () => {
|
|
945
|
+
const qty = btn.dataset.qty;
|
|
946
|
+
const input = document.getElementById('dfm-quantity');
|
|
947
|
+
if (input) input.value = qty;
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
}, 100);
|
|
951
|
+
|
|
952
|
+
// Export module
|
|
953
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
954
|
+
window.CycleCAD.Manufacturability = {
|
|
955
|
+
init,
|
|
956
|
+
getUI,
|
|
957
|
+
execute,
|
|
958
|
+
analyze: analyzeGeometry,
|
|
959
|
+
estimateCost,
|
|
960
|
+
generateReport,
|
|
961
|
+
createHeatmapOverlay,
|
|
962
|
+
MATERIALS,
|
|
963
|
+
PROCESS_RULES,
|
|
964
|
+
};
|