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.
@@ -0,0 +1,1222 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Killer Features Test Suite — cycleCAD</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: #0d1117;
17
+ color: #c9d1d9;
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .header {
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ margin-bottom: 24px;
32
+ padding-bottom: 16px;
33
+ border-bottom: 1px solid #30363d;
34
+ }
35
+
36
+ .header h1 {
37
+ font-size: 24px;
38
+ font-weight: 600;
39
+ color: #f0883e;
40
+ }
41
+
42
+ .header-meta {
43
+ display: flex;
44
+ gap: 16px;
45
+ align-items: center;
46
+ font-size: 14px;
47
+ }
48
+
49
+ .controls {
50
+ display: flex;
51
+ gap: 8px;
52
+ flex-wrap: wrap;
53
+ margin-bottom: 20px;
54
+ }
55
+
56
+ .btn {
57
+ padding: 8px 16px;
58
+ background: #238636;
59
+ border: none;
60
+ color: #fff;
61
+ border-radius: 4px;
62
+ cursor: pointer;
63
+ font-size: 13px;
64
+ font-weight: 600;
65
+ transition: background 0.2s;
66
+ }
67
+
68
+ .btn:hover {
69
+ background: #2ea043;
70
+ }
71
+
72
+ .btn.secondary {
73
+ background: #1f6feb;
74
+ }
75
+
76
+ .btn.secondary:hover {
77
+ background: #388bfd;
78
+ }
79
+
80
+ .btn.danger {
81
+ background: #da3633;
82
+ }
83
+
84
+ .btn.danger:hover {
85
+ background: #f85149;
86
+ }
87
+
88
+ .stats-grid {
89
+ display: grid;
90
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
91
+ gap: 12px;
92
+ margin-bottom: 20px;
93
+ }
94
+
95
+ .stat-card {
96
+ background: #161b22;
97
+ border: 1px solid #30363d;
98
+ border-radius: 4px;
99
+ padding: 16px;
100
+ text-align: center;
101
+ }
102
+
103
+ .stat-value {
104
+ font-size: 28px;
105
+ font-weight: 600;
106
+ color: #58a6ff;
107
+ margin-bottom: 4px;
108
+ }
109
+
110
+ .stat-label {
111
+ font-size: 12px;
112
+ color: #8b949e;
113
+ text-transform: uppercase;
114
+ letter-spacing: 0.5px;
115
+ }
116
+
117
+ .progress-bar {
118
+ height: 6px;
119
+ background: #30363d;
120
+ border-radius: 3px;
121
+ overflow: hidden;
122
+ margin-bottom: 20px;
123
+ }
124
+
125
+ .progress-fill {
126
+ height: 100%;
127
+ background: linear-gradient(90deg, #238636, #2ea043);
128
+ width: 0%;
129
+ transition: width 0.3s ease;
130
+ }
131
+
132
+ .test-sections {
133
+ display: grid;
134
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
135
+ gap: 16px;
136
+ }
137
+
138
+ .test-section {
139
+ background: #161b22;
140
+ border: 1px solid #30363d;
141
+ border-radius: 6px;
142
+ overflow: hidden;
143
+ display: flex;
144
+ flex-direction: column;
145
+ }
146
+
147
+ .section-header {
148
+ padding: 12px 16px;
149
+ background: #0d1117;
150
+ border-bottom: 1px solid #30363d;
151
+ font-weight: 600;
152
+ font-size: 13px;
153
+ color: #f0883e;
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: center;
157
+ }
158
+
159
+ .section-badge {
160
+ font-size: 11px;
161
+ padding: 2px 6px;
162
+ background: #238636;
163
+ color: #fff;
164
+ border-radius: 3px;
165
+ font-weight: 600;
166
+ }
167
+
168
+ .section-badge.partial {
169
+ background: #d29922;
170
+ }
171
+
172
+ .section-badge.fail {
173
+ background: #da3633;
174
+ }
175
+
176
+ .test-list {
177
+ flex: 1;
178
+ overflow-y: auto;
179
+ max-height: 400px;
180
+ }
181
+
182
+ .test-item {
183
+ padding: 10px 16px;
184
+ border-bottom: 1px solid #30363d;
185
+ font-size: 12px;
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 8px;
189
+ transition: background 0.2s;
190
+ }
191
+
192
+ .test-item:last-child {
193
+ border-bottom: none;
194
+ }
195
+
196
+ .test-item:hover {
197
+ background: #0d1117;
198
+ }
199
+
200
+ .test-item.pass {
201
+ color: #3fb950;
202
+ }
203
+
204
+ .test-item.fail {
205
+ color: #f85149;
206
+ }
207
+
208
+ .test-item.skip {
209
+ color: #d2a8ff;
210
+ }
211
+
212
+ .test-item.running {
213
+ color: #d29922;
214
+ font-weight: 600;
215
+ }
216
+
217
+ .test-icon {
218
+ min-width: 16px;
219
+ text-align: center;
220
+ }
221
+
222
+ .test-name {
223
+ flex: 1;
224
+ white-space: nowrap;
225
+ overflow: hidden;
226
+ text-overflow: ellipsis;
227
+ }
228
+
229
+ .test-time {
230
+ color: #8b949e;
231
+ font-size: 11px;
232
+ font-variant-numeric: tabular-nums;
233
+ }
234
+
235
+ .log-panel {
236
+ background: #161b22;
237
+ border: 1px solid #30363d;
238
+ border-radius: 6px;
239
+ padding: 16px;
240
+ margin-top: 20px;
241
+ max-height: 300px;
242
+ overflow-y: auto;
243
+ font-family: 'Monaco', 'Courier New', monospace;
244
+ font-size: 11px;
245
+ }
246
+
247
+ .log-item {
248
+ margin-bottom: 8px;
249
+ padding: 6px 8px;
250
+ background: #0d1117;
251
+ border-radius: 3px;
252
+ border-left: 2px solid #666;
253
+ line-height: 1.4;
254
+ }
255
+
256
+ .log-item.info {
257
+ color: #58a6ff;
258
+ border-left-color: #58a6ff;
259
+ }
260
+
261
+ .log-item.pass {
262
+ color: #3fb950;
263
+ border-left-color: #3fb950;
264
+ }
265
+
266
+ .log-item.fail {
267
+ color: #f85149;
268
+ border-left-color: #f85149;
269
+ }
270
+
271
+ .export-section {
272
+ display: flex;
273
+ gap: 8px;
274
+ margin-top: 20px;
275
+ }
276
+
277
+ .export-textarea {
278
+ flex: 1;
279
+ background: #0d1117;
280
+ border: 1px solid #30363d;
281
+ border-radius: 4px;
282
+ padding: 12px;
283
+ color: #c9d1d9;
284
+ font-family: 'Monaco', 'Courier New', monospace;
285
+ font-size: 11px;
286
+ resize: vertical;
287
+ min-height: 100px;
288
+ max-height: 200px;
289
+ }
290
+
291
+ .hidden {
292
+ display: none;
293
+ }
294
+ </style>
295
+ </head>
296
+ <body>
297
+ <div class="container">
298
+ <div class="header">
299
+ <div>
300
+ <h1>🧪 Killer Features Test Suite</h1>
301
+ <div style="font-size: 12px; color: #8b949e; margin-top: 4px;">
302
+ Text-to-CAD • Photo-to-CAD • Manufacturability Analysis
303
+ </div>
304
+ </div>
305
+ <div class="header-meta">
306
+ <div>
307
+ <div id="elapsed" style="font-size: 14px; font-weight: 600;">Ready</div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <div class="controls">
313
+ <button class="btn" id="btn-run-all">▶ Run All Tests</button>
314
+ <button class="btn secondary" id="btn-run-text2cad">Text-to-CAD</button>
315
+ <button class="btn secondary" id="btn-run-photo2cad">Photo-to-CAD</button>
316
+ <button class="btn secondary" id="btn-run-mfg">Manufacturability</button>
317
+ <button class="btn danger" id="btn-reset">Reset Results</button>
318
+ </div>
319
+
320
+ <div class="progress-bar">
321
+ <div class="progress-fill" id="progress"></div>
322
+ </div>
323
+
324
+ <div class="stats-grid">
325
+ <div class="stat-card">
326
+ <div class="stat-value" id="stat-pass">0</div>
327
+ <div class="stat-label">Passed</div>
328
+ </div>
329
+ <div class="stat-card">
330
+ <div class="stat-value" id="stat-fail">0</div>
331
+ <div class="stat-label">Failed</div>
332
+ </div>
333
+ <div class="stat-card">
334
+ <div class="stat-value" id="stat-skip">0</div>
335
+ <div class="stat-label">Skipped</div>
336
+ </div>
337
+ <div class="stat-card">
338
+ <div class="stat-value" id="stat-total">0</div>
339
+ <div class="stat-label">Total</div>
340
+ </div>
341
+ </div>
342
+
343
+ <div class="test-sections" id="test-sections"></div>
344
+
345
+ <div class="log-panel" id="log"></div>
346
+
347
+ <div class="export-section">
348
+ <textarea class="export-textarea" id="export-data" readonly placeholder="Export results as JSON..."></textarea>
349
+ <button class="btn" id="btn-export" style="align-self: flex-start;">📋 Export JSON</button>
350
+ </div>
351
+ </div>
352
+
353
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r170/three.min.js"></script>
354
+ <script>
355
+ // Mock modules for testing
356
+ const TextToCAD = {
357
+ name: 'text-to-cad',
358
+ init() {
359
+ this.initialized = true;
360
+ },
361
+ getUI() {
362
+ return document.createElement('div');
363
+ },
364
+ execute(action, params) {
365
+ if (action === 'parse') return this.parseDescription(params.input);
366
+ if (action === 'generate') return this.generateGeometry(params.spec);
367
+ return null;
368
+ },
369
+ parseDescription(input) {
370
+ const lower = input.toLowerCase();
371
+
372
+ // Units conversion
373
+ const inchMatch = input.match(/(\d+(?:\.\d+)?)\s*(?:inch|in|")/i);
374
+ const mmMultiplier = inchMatch ? 25.4 : 1;
375
+
376
+ // Shape detection
377
+ if (lower.includes('cylinder') || lower.includes('cylindar')) {
378
+ const diamMatch = input.match(/(\d+(?:\.\d+)?)\s*(?:mm|cm|inch|in)?.*(?:diameter|dia|d)/i) ||
379
+ input.match(/diameter\s*(\d+(?:\.\d+)?)/i);
380
+ const heightMatch = input.match(/(\d+(?:\.\d+)?)\s*(?:mm|cm|inch|in)?.*(?:tall|high|height|long)/i) ||
381
+ input.match(/height\s*(\d+(?:\.\d+)?)/i) ||
382
+ input.match(/long\s*(\d+(?:\.\d+)?)/i);
383
+ return {
384
+ type: 'cylinder',
385
+ diameter: diamMatch ? parseFloat(diamMatch[1]) * mmMultiplier : 50,
386
+ height: heightMatch ? parseFloat(heightMatch[1]) * mmMultiplier : 80,
387
+ confidence: 0.95
388
+ };
389
+ }
390
+
391
+ if (lower.includes('gear')) {
392
+ const teethMatch = input.match(/(\d+)\s*(?:teeth|tooth|t)/i);
393
+ const moduleMatch = input.match(/module\s*(\d+(?:\.\d+)?)/i);
394
+ return {
395
+ type: 'gear',
396
+ teeth: teethMatch ? parseInt(teethMatch[1]) : 20,
397
+ module: moduleMatch ? parseFloat(moduleMatch[1]) : 2,
398
+ confidence: 0.92
399
+ };
400
+ }
401
+
402
+ if (lower.includes('bracket') || lower.includes('l-bracket')) {
403
+ const dims = input.match(/(\d+)\s*x\s*(\d+)\s*x\s*(\d+)/);
404
+ if (dims) {
405
+ return {
406
+ type: 'bracket',
407
+ width: parseInt(dims[1]) * mmMultiplier,
408
+ height: parseInt(dims[2]) * mmMultiplier,
409
+ thickness: parseInt(dims[3]) * mmMultiplier,
410
+ confidence: 0.88
411
+ };
412
+ }
413
+ }
414
+
415
+ if (lower.includes('plate') || lower.includes('box') || lower.includes('rectangular')) {
416
+ const dims = input.match(/(\d+)\s*x\s*(\d+)\s*x\s*(\d+)/);
417
+ if (dims) {
418
+ return {
419
+ type: 'plate',
420
+ width: parseInt(dims[1]) * mmMultiplier,
421
+ depth: parseInt(dims[2]) * mmMultiplier,
422
+ height: parseInt(dims[3]) * mmMultiplier,
423
+ confidence: 0.90
424
+ };
425
+ }
426
+ }
427
+
428
+ if (lower.includes('hole') || lower.includes('holes')) {
429
+ const radiusMatch = input.match(/(?:radius|r)\s*(\d+(?:\.\d+)?)/i);
430
+ const countMatch = input.match(/(\d+)\s*(?:holes?|mounting)/i);
431
+ return {
432
+ type: 'hole',
433
+ radius: radiusMatch ? parseFloat(radiusMatch[1]) : 5,
434
+ count: countMatch ? parseInt(countMatch[1]) : 1,
435
+ confidence: 0.85
436
+ };
437
+ }
438
+
439
+ return {
440
+ type: 'unknown',
441
+ confidence: 0.2,
442
+ description: input
443
+ };
444
+ },
445
+ generateGeometry(spec) {
446
+ if (!spec) return null;
447
+
448
+ const group = new THREE.Group();
449
+
450
+ switch (spec.type) {
451
+ case 'cylinder':
452
+ group.add(new THREE.Mesh(
453
+ new THREE.CylinderGeometry(spec.diameter / 2, spec.diameter / 2, spec.height, 32),
454
+ new THREE.MeshPhongMaterial({ color: 0x4a90e2 })
455
+ ));
456
+ break;
457
+ case 'gear':
458
+ const geometry = this.createGearGeometry(spec.teeth, spec.module);
459
+ group.add(new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({ color: 0x7cb342 })));
460
+ break;
461
+ case 'plate':
462
+ group.add(new THREE.Mesh(
463
+ new THREE.BoxGeometry(spec.width, spec.height, spec.depth),
464
+ new THREE.MeshPhongMaterial({ color: 0xf57c00 })
465
+ ));
466
+ break;
467
+ case 'bracket':
468
+ group.add(new THREE.Mesh(
469
+ new THREE.BoxGeometry(spec.width, spec.height, spec.thickness),
470
+ new THREE.MeshPhongMaterial({ color: 0xd32f2f })
471
+ ));
472
+ break;
473
+ case 'hole':
474
+ group.add(new THREE.Mesh(
475
+ new THREE.CylinderGeometry(spec.radius, spec.radius, 10, 32),
476
+ new THREE.MeshPhongMaterial({ color: 0x9c27b0 })
477
+ ));
478
+ break;
479
+ default:
480
+ group.add(new THREE.Mesh(
481
+ new THREE.BoxGeometry(50, 50, 50),
482
+ new THREE.MeshPhongMaterial({ color: 0x999 })
483
+ ));
484
+ }
485
+
486
+ return group;
487
+ },
488
+ createGearGeometry(teeth, module) {
489
+ const radius = (teeth * module) / 2;
490
+ const geometry = new THREE.CylinderGeometry(radius, radius, 10, teeth, teeth);
491
+ return geometry;
492
+ }
493
+ };
494
+
495
+ const PhotoToCAD = {
496
+ name: 'photo-to-cad',
497
+ init() {
498
+ this.initialized = true;
499
+ this.canvas = document.createElement('canvas');
500
+ },
501
+ getUI() {
502
+ const div = document.createElement('div');
503
+ div.style.cssText = 'width: 300px; height: 200px; border: 2px dashed #666; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #666; font-size: 12px;';
504
+ div.textContent = 'Drop image here';
505
+ return div;
506
+ },
507
+ execute(action, params) {
508
+ if (action === 'detect') return this.processImage(params.dataUrl);
509
+ if (action === 'reconstruct') return this.reconstruct3D(params.edges);
510
+ return null;
511
+ },
512
+ processImage(dataUrl) {
513
+ if (!dataUrl) {
514
+ return { error: 'No image data', features: [] };
515
+ }
516
+
517
+ return {
518
+ features: this.detectEdges(dataUrl),
519
+ dimensions: { estimated: true },
520
+ confidence: 0.75
521
+ };
522
+ },
523
+ detectEdges(dataUrl) {
524
+ // Synthetic edge detection
525
+ if (!dataUrl) return [];
526
+
527
+ const features = [];
528
+
529
+ // Simulate circular edge detection
530
+ if (dataUrl.includes('circle') || Math.random() > 0.5) {
531
+ features.push({
532
+ type: 'circle',
533
+ radius: 25,
534
+ center: { x: 100, y: 100 }
535
+ });
536
+ }
537
+
538
+ // Simulate rectangular edge detection
539
+ if (dataUrl.includes('rect') || Math.random() > 0.4) {
540
+ features.push({
541
+ type: 'rectangle',
542
+ width: 150,
543
+ height: 100,
544
+ corner: { x: 50, y: 50 }
545
+ });
546
+ }
547
+
548
+ return features;
549
+ },
550
+ reconstruct3D(edges) {
551
+ if (!edges || edges.length === 0) {
552
+ return { error: 'No edges detected', geometry: null };
553
+ }
554
+
555
+ const group = new THREE.Group();
556
+
557
+ edges.forEach(edge => {
558
+ if (edge.type === 'circle') {
559
+ group.add(new THREE.Mesh(
560
+ new THREE.CylinderGeometry(edge.radius, edge.radius, 20, 32),
561
+ new THREE.MeshPhongMaterial({ color: 0x42a5f5 })
562
+ ));
563
+ } else if (edge.type === 'rectangle') {
564
+ group.add(new THREE.Mesh(
565
+ new THREE.BoxGeometry(edge.width, edge.height, 10),
566
+ new THREE.MeshPhongMaterial({ color: 0x66bb6a })
567
+ ));
568
+ }
569
+ });
570
+
571
+ return { geometry: group, features: edges.length, success: true };
572
+ }
573
+ };
574
+
575
+ const Manufacturability = {
576
+ name: 'manufacturability',
577
+ MATERIALS: {
578
+ aluminum_6061: { density: 2.7, cost_per_kg: 5.50, name: 'Aluminum 6061' },
579
+ steel_mild: { density: 7.85, cost_per_kg: 0.80, name: 'Steel (Mild)' },
580
+ stainless_304: { density: 8.0, cost_per_kg: 3.50, name: 'Stainless Steel 304' },
581
+ titanium_gr2: { density: 4.51, cost_per_kg: 15.00, name: 'Titanium Grade 2' },
582
+ plastic_abs: { density: 1.05, cost_per_kg: 2.20, name: 'ABS Plastic' },
583
+ plastic_pla: { density: 1.24, cost_per_kg: 1.80, name: 'PLA Plastic' },
584
+ plastic_nylon: { density: 1.14, cost_per_kg: 3.00, name: 'Nylon' },
585
+ plastic_peek: { density: 1.32, cost_per_kg: 25.00, name: 'PEEK' },
586
+ copper_pure: { density: 8.96, cost_per_kg: 8.50, name: 'Pure Copper' },
587
+ brass_c360: { density: 8.47, cost_per_kg: 6.00, name: 'Brass C360' },
588
+ magnesium_az91: { density: 1.81, cost_per_kg: 12.00, name: 'Magnesium AZ91' },
589
+ carbon_fiber: { density: 1.55, cost_per_kg: 20.00, name: 'Carbon Fiber' },
590
+ fiberglass_epoxy: { density: 1.85, cost_per_kg: 4.50, name: 'Fiberglass' },
591
+ wood_oak: { density: 0.75, cost_per_kg: 2.00, name: 'Oak Wood' },
592
+ ceramic_al2o3: { density: 3.95, cost_per_kg: 8.00, name: 'Alumina Ceramic' },
593
+ composite_cfrp: { density: 1.6, cost_per_kg: 18.00, name: 'CFRP Composite' },
594
+ rubber_silicone: { density: 1.1, cost_per_kg: 5.50, name: 'Silicone Rubber' },
595
+ foam_pu: { density: 0.03, cost_per_kg: 3.00, name: 'Polyurethane Foam' },
596
+ glass_borosilicate: { density: 2.23, cost_per_kg: 4.00, name: 'Borosilicate Glass' },
597
+ concrete_standard: { density: 2.4, cost_per_kg: 0.15, name: 'Standard Concrete' }
598
+ },
599
+ PROCESS_RULES: {
600
+ CNC: { min_corner_radius: 0.5, min_wall_thickness: 1.0, time_per_mm3: 0.001 },
601
+ FDM: { min_wall_thickness: 1.2, min_feature_size: 2.0, support_angle: 45 },
602
+ SLA: { min_feature_size: 0.025, min_wall_thickness: 0.5, support_angle: 45 },
603
+ injection_molding: { min_draft_angle: 1.5, min_wall_thickness: 1.5, min_radius: 0.5 },
604
+ sheet_metal: { min_radius: 0.5, min_hole_distance: 5.0, max_bend_angle: 180 },
605
+ casting: { min_wall_thickness: 3.0, min_draft_angle: 2.0, fillet_radius: 2.0 }
606
+ },
607
+ init() {
608
+ this.initialized = true;
609
+ },
610
+ getUI() {
611
+ const div = document.createElement('div');
612
+ div.style.cssText = 'padding: 12px; background: #161b22; border-radius: 4px; font-size: 12px;';
613
+ div.innerHTML = '<strong>Manufacturability Analysis</strong><br/>Ready for geometry analysis.';
614
+ return div;
615
+ },
616
+ execute(action, params) {
617
+ if (action === 'analyze') return this.analyze(params.mesh);
618
+ if (action === 'cost') return this.estimateCost(params.mesh, params.material, params.quantity);
619
+ if (action === 'report') return this.generateReport(params.issues);
620
+ return null;
621
+ },
622
+ analyze(mesh) {
623
+ if (!mesh || !(mesh instanceof THREE.Mesh) && !(mesh instanceof THREE.Group)) {
624
+ return { error: 'Invalid geometry', issues: [] };
625
+ }
626
+
627
+ const issues = [];
628
+
629
+ // Simulate geometry analysis
630
+ const box = new THREE.Box3().setFromObject(mesh);
631
+ const size = box.getSize(new THREE.Vector3());
632
+
633
+ // Check wall thickness (simplified)
634
+ if (size.z < 0.5) {
635
+ issues.push({
636
+ type: 'wall_thickness',
637
+ severity: 'error',
638
+ message: 'Wall thickness too thin for standard manufacturing',
639
+ value: size.z
640
+ });
641
+ }
642
+
643
+ // Check corner radius
644
+ if (Math.min(size.x, size.y, size.z) < 1) {
645
+ issues.push({
646
+ type: 'corner_radius',
647
+ severity: 'warning',
648
+ message: 'Very small features may be difficult to manufacture',
649
+ value: Math.min(size.x, size.y, size.z)
650
+ });
651
+ }
652
+
653
+ // Check aspect ratio
654
+ const maxDim = Math.max(size.x, size.y, size.z);
655
+ const minDim = Math.min(size.x, size.y, size.z);
656
+ if (maxDim / minDim > 10) {
657
+ issues.push({
658
+ type: 'aspect_ratio',
659
+ severity: 'info',
660
+ message: 'High aspect ratio may affect tolerances',
661
+ ratio: maxDim / minDim
662
+ });
663
+ }
664
+
665
+ return { success: true, issues: issues, geometry: { volume: size.x * size.y * size.z } };
666
+ },
667
+ estimateCost(mesh, material = 'steel_mild', quantity = 1) {
668
+ if (!mesh) {
669
+ return { error: 'Invalid geometry', costs: {} };
670
+ }
671
+
672
+ const mat = this.MATERIALS[material] || this.MATERIALS.steel_mild;
673
+ const box = new THREE.Box3().setFromObject(mesh);
674
+ const size = box.getSize(new THREE.Vector3());
675
+ const volume = size.x * size.y * size.z; // Simplified
676
+ const mass = volume * mat.density / 1000; // grams to kg
677
+ const baseCost = mass * mat.cost_per_kg;
678
+
679
+ // Quantity discounts
680
+ const quantityMultiplier = quantity >= 1000 ? 0.5 : quantity >= 100 ? 0.7 : 1.0;
681
+
682
+ return {
683
+ success: true,
684
+ material: mat.name,
685
+ mass: mass.toFixed(2),
686
+ unit_cost: (baseCost * quantityMultiplier).toFixed(2),
687
+ total_cost: (baseCost * quantity * quantityMultiplier).toFixed(2),
688
+ quantity: quantity,
689
+ processes: {
690
+ CNC: (baseCost * 1.2).toFixed(2),
691
+ FDM: (baseCost * 0.5).toFixed(2),
692
+ injection_molding: (baseCost * 2.0).toFixed(2)
693
+ }
694
+ };
695
+ },
696
+ generateReport(issues) {
697
+ if (!Array.isArray(issues)) issues = [];
698
+
699
+ let html = '<div style="background: #0d1117; padding: 12px; border-radius: 4px; font-size: 11px;">';
700
+ html += '<strong>Manufacturing Analysis Report</strong><br/>';
701
+ html += `<div style="margin-top: 8px;">Issues: ${issues.length}</div>`;
702
+ issues.slice(0, 3).forEach(issue => {
703
+ html += `<div style="margin-top: 6px; color: ${issue.severity === 'error' ? '#f85149' : '#d29922'};">
704
+ • ${issue.message}
705
+ </div>`;
706
+ });
707
+ html += '</div>';
708
+
709
+ return html;
710
+ }
711
+ };
712
+
713
+ // Test Suite Runner
714
+ class TestSuite {
715
+ constructor() {
716
+ this.stats = { pass: 0, fail: 0, skip: 0, total: 0 };
717
+ this.tests = [];
718
+ this.startTime = 0;
719
+ this.results = [];
720
+ }
721
+
722
+ async run(moduleTests) {
723
+ this.startTime = Date.now();
724
+ const sections = {};
725
+
726
+ for (const test of moduleTests) {
727
+ if (!sections[test.section]) {
728
+ sections[test.section] = { pass: 0, fail: 0, total: 0, tests: [] };
729
+ }
730
+
731
+ try {
732
+ await this.runTest(test, sections[test.section]);
733
+ } catch (err) {
734
+ this.fail(test.name, err.message, sections[test.section]);
735
+ }
736
+ }
737
+
738
+ this.updateUI(sections);
739
+ return { stats: this.stats, sections: sections };
740
+ }
741
+
742
+ async runTest(test, section) {
743
+ const log = document.getElementById('log');
744
+ const item = document.createElement('div');
745
+ item.className = 'log-item info';
746
+ item.textContent = `⏳ ${test.name}...`;
747
+ log.appendChild(item);
748
+ log.scrollTop = log.scrollHeight;
749
+
750
+ try {
751
+ const result = await Promise.race([
752
+ test.fn(),
753
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout (5s)')), 5000))
754
+ ]);
755
+
756
+ if (result === false) {
757
+ this.fail(test.name, 'Assertion failed', section);
758
+ } else {
759
+ this.pass(test.name, section);
760
+ }
761
+ } catch (err) {
762
+ this.fail(test.name, err.message, section);
763
+ }
764
+
765
+ this.updateProgress();
766
+ }
767
+
768
+ pass(name, section) {
769
+ this.stats.pass++;
770
+ this.stats.total++;
771
+ section.pass++;
772
+ section.total++;
773
+
774
+ const log = document.getElementById('log');
775
+ const items = log.querySelectorAll('.log-item');
776
+ const lastItem = items[items.length - 1];
777
+ if (lastItem && lastItem.textContent.includes(name)) {
778
+ lastItem.className = 'log-item pass';
779
+ lastItem.textContent = `✓ ${name}`;
780
+ }
781
+
782
+ this.results.push({ name, status: 'pass' });
783
+ }
784
+
785
+ fail(name, reason, section) {
786
+ this.stats.fail++;
787
+ this.stats.total++;
788
+ section.fail++;
789
+ section.total++;
790
+
791
+ const log = document.getElementById('log');
792
+ const items = log.querySelectorAll('.log-item');
793
+ const lastItem = items[items.length - 1];
794
+ if (lastItem && lastItem.textContent.includes(name)) {
795
+ lastItem.className = 'log-item fail';
796
+ lastItem.textContent = `✗ ${name} — ${reason}`;
797
+ }
798
+
799
+ this.results.push({ name, status: 'fail', reason });
800
+ }
801
+
802
+ updateProgress() {
803
+ const pct = (this.stats.total / (this.stats.total + 100)) * 100; // Estimate
804
+ document.getElementById('progress').style.width = Math.min(pct, 95) + '%';
805
+ document.getElementById('stat-pass').textContent = this.stats.pass;
806
+ document.getElementById('stat-fail').textContent = this.stats.fail;
807
+ document.getElementById('stat-total').textContent = this.stats.total;
808
+ }
809
+
810
+ updateUI(sections) {
811
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2);
812
+ document.getElementById('elapsed').textContent = `${elapsed}s`;
813
+ document.getElementById('progress').style.width = '100%';
814
+
815
+ const container = document.getElementById('test-sections');
816
+ container.innerHTML = '';
817
+
818
+ for (const [sectionName, data] of Object.entries(sections)) {
819
+ const section = document.createElement('div');
820
+ section.className = 'test-section';
821
+
822
+ const passRate = data.total > 0 ? (data.pass / data.total * 100).toFixed(0) : 0;
823
+ let badgeClass = 'section-badge';
824
+ if (data.fail > 0) badgeClass += ' fail';
825
+ else if (data.pass < data.total) badgeClass += ' partial';
826
+
827
+ section.innerHTML = `
828
+ <div class="section-header">
829
+ <span>${sectionName}</span>
830
+ <span class="${badgeClass}">${data.pass}/${data.total} (${passRate}%)</span>
831
+ </div>
832
+ <div class="test-list">${data.tests.map(t => `
833
+ <div class="test-item ${t.status}">
834
+ <span class="test-icon">${t.status === 'pass' ? '✓' : t.status === 'fail' ? '✗' : '○'}</span>
835
+ <span class="test-name">${t.name}</span>
836
+ <span class="test-time">${t.time}ms</span>
837
+ </div>
838
+ `).join('')}</div>
839
+ `;
840
+ container.appendChild(section);
841
+ }
842
+
843
+ this.exportResults();
844
+ }
845
+
846
+ exportResults() {
847
+ const data = {
848
+ timestamp: new Date().toISOString(),
849
+ duration: ((Date.now() - this.startTime) / 1000).toFixed(2),
850
+ stats: this.stats,
851
+ results: this.results
852
+ };
853
+ document.getElementById('export-data').value = JSON.stringify(data, null, 2);
854
+ }
855
+ }
856
+
857
+ // Define all tests
858
+ const TEXT_TO_CAD_TESTS = [
859
+ {
860
+ name: 'Module exists and exports API',
861
+ section: 'Text-to-CAD',
862
+ fn: () => TextToCAD.init() && TextToCAD.execute && TextToCAD.parseDescription && TextToCAD.generateGeometry
863
+ },
864
+ {
865
+ name: 'parseDescription: cylinder 50mm diameter 80mm tall',
866
+ section: 'Text-to-CAD',
867
+ fn: () => {
868
+ const result = TextToCAD.parseDescription('a cylinder 50mm diameter 80mm tall');
869
+ return result.type === 'cylinder' && result.diameter === 50 && result.height === 80;
870
+ }
871
+ },
872
+ {
873
+ name: 'parseDescription: gear with 24 teeth module 2',
874
+ section: 'Text-to-CAD',
875
+ fn: () => {
876
+ const result = TextToCAD.parseDescription('gear with 24 teeth module 2');
877
+ return result.type === 'gear' && result.teeth === 24 && result.module === 2;
878
+ }
879
+ },
880
+ {
881
+ name: 'parseDescription: L-bracket 100x60x5mm with holes',
882
+ section: 'Text-to-CAD',
883
+ fn: () => {
884
+ const result = TextToCAD.parseDescription('L-bracket 100x60x5mm with 2 mounting holes');
885
+ return result.type === 'bracket' && result.width === 100;
886
+ }
887
+ },
888
+ {
889
+ name: 'parseDescription: plate 200x100x10mm',
890
+ section: 'Text-to-CAD',
891
+ fn: () => {
892
+ const result = TextToCAD.parseDescription('plate 200x100x10mm');
893
+ return result.type === 'plate' && result.width === 200;
894
+ }
895
+ },
896
+ {
897
+ name: 'Unit conversion: inches to mm',
898
+ section: 'Text-to-CAD',
899
+ fn: () => {
900
+ const result = TextToCAD.parseDescription('cylinder 2 inch diameter');
901
+ return result.diameter > 45 && result.diameter < 55; // ~50.8mm
902
+ }
903
+ },
904
+ {
905
+ name: 'generateGeometry returns THREE.Group or Mesh',
906
+ section: 'Text-to-CAD',
907
+ fn: () => {
908
+ const spec = { type: 'cylinder', diameter: 50, height: 80 };
909
+ const geom = TextToCAD.generateGeometry(spec);
910
+ return geom instanceof THREE.Group || geom instanceof THREE.Mesh;
911
+ }
912
+ },
913
+ {
914
+ name: 'getUI returns HTMLElement',
915
+ section: 'Text-to-CAD',
916
+ fn: () => {
917
+ const ui = TextToCAD.getUI();
918
+ return ui instanceof HTMLElement;
919
+ }
920
+ },
921
+ {
922
+ name: 'execute parse returns spec object',
923
+ section: 'Text-to-CAD',
924
+ fn: () => {
925
+ const spec = TextToCAD.execute('parse', { input: 'box 50mm' });
926
+ return spec && spec.type;
927
+ }
928
+ },
929
+ {
930
+ name: 'execute generate returns geometry',
931
+ section: 'Text-to-CAD',
932
+ fn: () => {
933
+ const spec = { type: 'cylinder', diameter: 50, height: 80 };
934
+ const geom = TextToCAD.execute('generate', { spec });
935
+ return geom instanceof THREE.Group || geom instanceof THREE.Mesh;
936
+ }
937
+ },
938
+ {
939
+ name: 'Multi-step: cylinder → hole → check children',
940
+ section: 'Text-to-CAD',
941
+ fn: () => {
942
+ const cyl = TextToCAD.generateGeometry({ type: 'cylinder', diameter: 50, height: 80 });
943
+ const hole = TextToCAD.generateGeometry({ type: 'hole', radius: 10, count: 1 });
944
+ return cyl && hole && (cyl.children ? cyl.children.length >= 1 : true);
945
+ }
946
+ },
947
+ {
948
+ name: 'Unknown input returns low confidence',
949
+ section: 'Text-to-CAD',
950
+ fn: () => {
951
+ const result = TextToCAD.parseDescription('xyzabc qwerty nonsense');
952
+ return result.type === 'unknown' && result.confidence < 0.5;
953
+ }
954
+ },
955
+ {
956
+ name: 'Empty input returns gracefully',
957
+ section: 'Text-to-CAD',
958
+ fn: () => {
959
+ const result = TextToCAD.parseDescription('');
960
+ return result && result.type !== undefined;
961
+ }
962
+ },
963
+ {
964
+ name: 'Unit extraction: cm, mm, inch all work',
965
+ section: 'Text-to-CAD',
966
+ fn: () => {
967
+ const mm = TextToCAD.parseDescription('cylinder 50mm diameter');
968
+ const inch = TextToCAD.parseDescription('cylinder 2 inch diameter');
969
+ return mm.diameter === 50 && inch.diameter > 40;
970
+ }
971
+ },
972
+ {
973
+ name: 'Feature detection: fillets, chamfers, patterns',
974
+ section: 'Text-to-CAD',
975
+ fn: () => {
976
+ const r1 = TextToCAD.parseDescription('cylinder with fillet');
977
+ const r2 = TextToCAD.parseDescription('plate with chamfer');
978
+ return r1 && r2;
979
+ }
980
+ }
981
+ ];
982
+
983
+ const PHOTO_TO_CAD_TESTS = [
984
+ {
985
+ name: 'Module exists and exports API',
986
+ section: 'Photo-to-CAD',
987
+ fn: () => PhotoToCAD.init() && PhotoToCAD.execute && PhotoToCAD.processImage && PhotoToCAD.detectEdges
988
+ },
989
+ {
990
+ name: 'getUI returns HTMLElement',
991
+ section: 'Photo-to-CAD',
992
+ fn: () => {
993
+ const ui = PhotoToCAD.getUI();
994
+ return ui instanceof HTMLElement;
995
+ }
996
+ },
997
+ {
998
+ name: 'init() completes without error',
999
+ section: 'Photo-to-CAD',
1000
+ fn: () => {
1001
+ PhotoToCAD.init();
1002
+ return PhotoToCAD.initialized === true;
1003
+ }
1004
+ },
1005
+ {
1006
+ name: 'detectEdges handles missing image gracefully',
1007
+ section: 'Photo-to-CAD',
1008
+ fn: () => {
1009
+ const edges = PhotoToCAD.detectEdges(null);
1010
+ return Array.isArray(edges);
1011
+ }
1012
+ },
1013
+ {
1014
+ name: 'reconstruct3D with no edges returns error',
1015
+ section: 'Photo-to-CAD',
1016
+ fn: () => {
1017
+ const result = PhotoToCAD.reconstruct3D([]);
1018
+ return result && result.error !== undefined;
1019
+ }
1020
+ },
1021
+ {
1022
+ name: 'Edge detection on synthetic circle',
1023
+ section: 'Photo-to-CAD',
1024
+ fn: () => {
1025
+ const edges = PhotoToCAD.detectEdges('data:circle');
1026
+ return Array.isArray(edges) && edges.some(e => e.type === 'circle');
1027
+ }
1028
+ },
1029
+ {
1030
+ name: 'Edge detection on synthetic rectangle',
1031
+ section: 'Photo-to-CAD',
1032
+ fn: () => {
1033
+ const edges = PhotoToCAD.detectEdges('data:rect');
1034
+ return Array.isArray(edges) && edges.some(e => e.type === 'rectangle');
1035
+ }
1036
+ },
1037
+ {
1038
+ name: 'processImage with valid data URL works',
1039
+ section: 'Photo-to-CAD',
1040
+ fn: () => {
1041
+ const result = PhotoToCAD.processImage('data:image/png;base64,iVBORw0KGgo=');
1042
+ return result && !result.error && Array.isArray(result.features);
1043
+ }
1044
+ },
1045
+ {
1046
+ name: 'execute detect with no image returns graceful error',
1047
+ section: 'Photo-to-CAD',
1048
+ fn: () => {
1049
+ const result = PhotoToCAD.execute('detect', {});
1050
+ return result && result.error !== undefined;
1051
+ }
1052
+ },
1053
+ {
1054
+ name: 'Export results contain expected fields',
1055
+ section: 'Photo-to-CAD',
1056
+ fn: () => {
1057
+ const result = PhotoToCAD.processImage('data:test');
1058
+ return result && result.features !== undefined && result.dimensions !== undefined;
1059
+ }
1060
+ }
1061
+ ];
1062
+
1063
+ const MANUFACTURABILITY_TESTS = [
1064
+ {
1065
+ name: 'Module exists and exports API',
1066
+ section: 'Manufacturability',
1067
+ fn: () => Manufacturability.init() && Manufacturability.execute && Manufacturability.analyze
1068
+ },
1069
+ {
1070
+ name: 'MATERIALS database has 20+ materials',
1071
+ section: 'Manufacturability',
1072
+ fn: () => Object.keys(Manufacturability.MATERIALS).length >= 20
1073
+ },
1074
+ {
1075
+ name: 'MATERIALS[aluminum_6061] has correct properties',
1076
+ section: 'Manufacturability',
1077
+ fn: () => {
1078
+ const mat = Manufacturability.MATERIALS.aluminum_6061;
1079
+ return mat && mat.density === 2.7 && mat.cost_per_kg === 5.50;
1080
+ }
1081
+ },
1082
+ {
1083
+ name: 'PROCESS_RULES has entries for all major processes',
1084
+ section: 'Manufacturability',
1085
+ fn: () => {
1086
+ const rules = Manufacturability.PROCESS_RULES;
1087
+ return rules.CNC && rules.FDM && rules.injection_molding && rules.sheet_metal;
1088
+ }
1089
+ },
1090
+ {
1091
+ name: 'analyze() returns issues array',
1092
+ section: 'Manufacturability',
1093
+ fn: () => {
1094
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 1));
1095
+ const result = Manufacturability.analyze(mesh);
1096
+ return Array.isArray(result.issues);
1097
+ }
1098
+ },
1099
+ {
1100
+ name: 'analyze() flags thin-wall issues',
1101
+ section: 'Manufacturability',
1102
+ fn: () => {
1103
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 0.2));
1104
+ const result = Manufacturability.analyze(mesh);
1105
+ return result.issues && result.issues.some(i => i.type === 'wall_thickness');
1106
+ }
1107
+ },
1108
+ {
1109
+ name: 'estimateCost returns cost object with process keys',
1110
+ section: 'Manufacturability',
1111
+ fn: () => {
1112
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 10));
1113
+ const cost = Manufacturability.estimateCost(mesh);
1114
+ return cost.processes && cost.processes.CNC && cost.processes.FDM;
1115
+ }
1116
+ },
1117
+ {
1118
+ name: 'estimateCost includes quantity discounts',
1119
+ section: 'Manufacturability',
1120
+ fn: () => {
1121
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 10));
1122
+ const cost1 = Manufacturability.estimateCost(mesh, 'steel_mild', 1);
1123
+ const cost100 = Manufacturability.estimateCost(mesh, 'steel_mild', 100);
1124
+ return parseFloat(cost100.unit_cost) < parseFloat(cost1.unit_cost);
1125
+ }
1126
+ },
1127
+ {
1128
+ name: 'getUI returns HTMLElement',
1129
+ section: 'Manufacturability',
1130
+ fn: () => {
1131
+ const ui = Manufacturability.getUI();
1132
+ return ui instanceof HTMLElement;
1133
+ }
1134
+ },
1135
+ {
1136
+ name: 'generateReport returns HTML string',
1137
+ section: 'Manufacturability',
1138
+ fn: () => {
1139
+ const html = Manufacturability.generateReport([{ severity: 'warning', message: 'Test' }]);
1140
+ return typeof html === 'string' && html.includes('<');
1141
+ }
1142
+ },
1143
+ {
1144
+ name: 'Cost for 1 unit > cost per unit for 1000 units',
1145
+ section: 'Manufacturability',
1146
+ fn: () => {
1147
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 10));
1148
+ const c1 = Manufacturability.estimateCost(mesh, 'aluminum_6061', 1);
1149
+ const c1000 = Manufacturability.estimateCost(mesh, 'aluminum_6061', 1000);
1150
+ return parseFloat(c1.unit_cost) > parseFloat(c1000.unit_cost);
1151
+ }
1152
+ },
1153
+ {
1154
+ name: 'CNC milling rules have min_corner_radius',
1155
+ section: 'Manufacturability',
1156
+ fn: () => Manufacturability.PROCESS_RULES.CNC && Manufacturability.PROCESS_RULES.CNC.min_corner_radius
1157
+ },
1158
+ {
1159
+ name: 'FDM rules have min_wall_thickness',
1160
+ section: 'Manufacturability',
1161
+ fn: () => Manufacturability.PROCESS_RULES.FDM && Manufacturability.PROCESS_RULES.FDM.min_wall_thickness
1162
+ },
1163
+ {
1164
+ name: 'Injection molding rules have min_draft_angle',
1165
+ section: 'Manufacturability',
1166
+ fn: () => Manufacturability.PROCESS_RULES.injection_molding && Manufacturability.PROCESS_RULES.injection_molding.min_draft_angle
1167
+ },
1168
+ {
1169
+ name: 'Analyze with no geometry returns graceful error',
1170
+ section: 'Manufacturability',
1171
+ fn: () => {
1172
+ const result = Manufacturability.analyze(null);
1173
+ return result && result.error !== undefined;
1174
+ }
1175
+ }
1176
+ ];
1177
+
1178
+ // Initialize UI
1179
+ const suite = new TestSuite();
1180
+
1181
+ document.getElementById('btn-run-all').addEventListener('click', () => {
1182
+ document.getElementById('log').innerHTML = '';
1183
+ suite.run([...TEXT_TO_CAD_TESTS, ...PHOTO_TO_CAD_TESTS, ...MANUFACTURABILITY_TESTS]);
1184
+ });
1185
+
1186
+ document.getElementById('btn-run-text2cad').addEventListener('click', () => {
1187
+ document.getElementById('log').innerHTML = '';
1188
+ suite.run(TEXT_TO_CAD_TESTS);
1189
+ });
1190
+
1191
+ document.getElementById('btn-run-photo2cad').addEventListener('click', () => {
1192
+ document.getElementById('log').innerHTML = '';
1193
+ suite.run(PHOTO_TO_CAD_TESTS);
1194
+ });
1195
+
1196
+ document.getElementById('btn-run-mfg').addEventListener('click', () => {
1197
+ document.getElementById('log').innerHTML = '';
1198
+ suite.run(MANUFACTURABILITY_TESTS);
1199
+ });
1200
+
1201
+ document.getElementById('btn-reset').addEventListener('click', () => {
1202
+ suite.stats = { pass: 0, fail: 0, skip: 0, total: 0 };
1203
+ suite.results = [];
1204
+ document.getElementById('log').innerHTML = '';
1205
+ document.getElementById('test-sections').innerHTML = '';
1206
+ document.getElementById('stat-pass').textContent = '0';
1207
+ document.getElementById('stat-fail').textContent = '0';
1208
+ document.getElementById('stat-skip').textContent = '0';
1209
+ document.getElementById('stat-total').textContent = '0';
1210
+ document.getElementById('progress').style.width = '0%';
1211
+ document.getElementById('export-data').value = '';
1212
+ });
1213
+
1214
+ document.getElementById('btn-export').addEventListener('click', () => {
1215
+ const textarea = document.getElementById('export-data');
1216
+ textarea.select();
1217
+ document.execCommand('copy');
1218
+ alert('Results copied to clipboard!');
1219
+ });
1220
+ </script>
1221
+ </body>
1222
+ </html>