cyclecad 3.6.0 → 3.8.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/HELP-QUICK-START.md +207 -0
- package/app/HELP-SYSTEM-README.md +287 -0
- package/app/help-viewer.html +805 -0
- package/app/index.html +48 -0
- package/app/js/killer-features-help.json +310 -391
- package/app/js/modules/generative-design.js +1102 -0
- package/app/js/modules/manufacturability.js +170 -3
- package/app/js/modules/multi-physics.js +1404 -0
- package/app/js/modules/photo-to-cad.js +200 -10
- package/app/js/modules/smart-parts.js +1925 -0
- package/app/js/modules/text-to-cad.js +242 -33
- package/app/tests/KILLER_FEATURES_BATCH2_README.md +214 -0
- package/app/tests/KILLER_FEATURES_TEST_GUIDE.md +324 -0
- package/app/tests/index.html +24 -7
- package/app/tests/killer-features-batch2-tests.html +849 -0
- package/app/tests/killer-features-visual-test.html +1362 -0
- package/docs/KILLER-FEATURES-GUIDE.md +2728 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +1663 -5
- package/package.json +1 -1
|
@@ -0,0 +1,849 @@
|
|
|
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>cycleCAD Killer Features Batch 2 Tests</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: #0f172a;
|
|
17
|
+
color: #e2e8f0;
|
|
18
|
+
display: flex;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
gap: 1px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.app-container {
|
|
24
|
+
flex: 0 0 65%;
|
|
25
|
+
background: #1a202c;
|
|
26
|
+
border: 1px solid #2d3748;
|
|
27
|
+
position: relative;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#test-canvas {
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 100%;
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.test-panel {
|
|
38
|
+
flex: 0 0 35%;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
background: #1a202c;
|
|
42
|
+
border-left: 1px solid #2d3748;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.test-header {
|
|
47
|
+
padding: 16px;
|
|
48
|
+
border-bottom: 1px solid #2d3748;
|
|
49
|
+
background: #0f172a;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.test-title {
|
|
53
|
+
font-size: 18px;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
margin-bottom: 12px;
|
|
56
|
+
color: #38bdf8;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.test-subtitle {
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
color: #64748b;
|
|
62
|
+
margin-bottom: 8px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.test-stats {
|
|
66
|
+
display: grid;
|
|
67
|
+
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
68
|
+
gap: 8px;
|
|
69
|
+
font-size: 12px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.stat-box {
|
|
73
|
+
padding: 8px;
|
|
74
|
+
background: #1e293b;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
border-left: 3px solid #64748b;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.stat-box.pass {
|
|
80
|
+
border-left-color: #10b981;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.stat-box.fail {
|
|
84
|
+
border-left-color: #ef4444;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.stat-box.skip {
|
|
88
|
+
border-left-color: #f59e0b;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stat-box.active {
|
|
92
|
+
border-left-color: #3b82f6;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.stat-label {
|
|
96
|
+
font-size: 11px;
|
|
97
|
+
color: #94a3b8;
|
|
98
|
+
margin-bottom: 2px;
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
letter-spacing: 0.5px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.stat-value {
|
|
104
|
+
font-size: 18px;
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.test-controls {
|
|
109
|
+
display: flex;
|
|
110
|
+
gap: 8px;
|
|
111
|
+
padding: 12px;
|
|
112
|
+
border-bottom: 1px solid #2d3748;
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
button {
|
|
117
|
+
padding: 8px 12px;
|
|
118
|
+
background: #1e293b;
|
|
119
|
+
color: #e2e8f0;
|
|
120
|
+
border: 1px solid #3f4655;
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
font-weight: 500;
|
|
125
|
+
transition: all 0.2s ease;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
button:hover {
|
|
129
|
+
background: #2d3748;
|
|
130
|
+
border-color: #4f5665;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
button.primary {
|
|
134
|
+
background: #0284c7;
|
|
135
|
+
border-color: #0284c7;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
button.primary:hover {
|
|
139
|
+
background: #0369a1;
|
|
140
|
+
border-color: #0369a1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
button:disabled {
|
|
144
|
+
opacity: 0.5;
|
|
145
|
+
cursor: not-allowed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.test-log {
|
|
149
|
+
flex: 1;
|
|
150
|
+
overflow-y: auto;
|
|
151
|
+
padding: 8px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.test-entry {
|
|
155
|
+
margin-bottom: 8px;
|
|
156
|
+
padding: 8px;
|
|
157
|
+
background: #1e293b;
|
|
158
|
+
border-left: 3px solid #64748b;
|
|
159
|
+
border-radius: 2px;
|
|
160
|
+
font-size: 12px;
|
|
161
|
+
line-height: 1.4;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.test-entry.pass {
|
|
165
|
+
border-left-color: #10b981;
|
|
166
|
+
background: #1a3a2a;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.test-entry.fail {
|
|
170
|
+
border-left-color: #ef4444;
|
|
171
|
+
background: #3a1a1a;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.test-entry.skip {
|
|
175
|
+
border-left-color: #f59e0b;
|
|
176
|
+
background: #3a2a1a;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.test-entry.running {
|
|
180
|
+
border-left-color: #3b82f6;
|
|
181
|
+
background: #1a2a3a;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.test-entry-title {
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
color: #cbd5e1;
|
|
187
|
+
margin-bottom: 2px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.test-entry-msg {
|
|
191
|
+
color: #94a3b8;
|
|
192
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.test-footer {
|
|
196
|
+
padding: 12px;
|
|
197
|
+
border-top: 1px solid #2d3748;
|
|
198
|
+
background: #0f172a;
|
|
199
|
+
font-size: 12px;
|
|
200
|
+
color: #64748b;
|
|
201
|
+
display: flex;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
align-items: center;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.elapsed-time {
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
color: #38bdf8;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.category-group {
|
|
212
|
+
margin-bottom: 4px;
|
|
213
|
+
padding: 4px;
|
|
214
|
+
background: #0f172a;
|
|
215
|
+
border-radius: 3px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.category-name {
|
|
219
|
+
font-size: 11px;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
color: #64748b;
|
|
222
|
+
text-transform: uppercase;
|
|
223
|
+
letter-spacing: 0.5px;
|
|
224
|
+
margin-bottom: 4px;
|
|
225
|
+
padding: 4px 4px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.progress-bar {
|
|
229
|
+
width: 100%;
|
|
230
|
+
height: 4px;
|
|
231
|
+
background: #1e293b;
|
|
232
|
+
border-radius: 2px;
|
|
233
|
+
margin: 8px 0;
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.progress-fill {
|
|
238
|
+
height: 100%;
|
|
239
|
+
background: linear-gradient(90deg, #10b981, #0284c7);
|
|
240
|
+
transition: width 0.3s ease;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.export-btn {
|
|
244
|
+
padding: 6px 10px;
|
|
245
|
+
font-size: 11px;
|
|
246
|
+
background: #047857;
|
|
247
|
+
border-color: #047857;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.export-btn:hover {
|
|
251
|
+
background: #059669;
|
|
252
|
+
border-color: #059669;
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
255
|
+
</head>
|
|
256
|
+
<body>
|
|
257
|
+
<div class="app-container">
|
|
258
|
+
<canvas id="test-canvas"></canvas>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="test-panel">
|
|
262
|
+
<div class="test-header">
|
|
263
|
+
<div class="test-title">Killer Features Batch 2</div>
|
|
264
|
+
<div class="test-subtitle">Generative Design, Multi-Physics, Smart Parts</div>
|
|
265
|
+
<div class="progress-bar" style="margin-top: 8px;">
|
|
266
|
+
<div class="progress-fill" id="progress-fill" style="width: 0%;"></div>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="test-stats">
|
|
269
|
+
<div class="stat-box pass">
|
|
270
|
+
<div class="stat-label">Pass</div>
|
|
271
|
+
<div class="stat-value" id="stat-pass">0</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="stat-box fail">
|
|
274
|
+
<div class="stat-label">Fail</div>
|
|
275
|
+
<div class="stat-value" id="stat-fail">0</div>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="stat-box skip">
|
|
278
|
+
<div class="stat-label">Skip</div>
|
|
279
|
+
<div class="stat-value" id="stat-skip">0</div>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="stat-box active">
|
|
282
|
+
<div class="stat-label">Total</div>
|
|
283
|
+
<div class="stat-value" id="stat-total">36</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div class="test-controls">
|
|
289
|
+
<button class="primary" id="run-all-btn">Run All Tests</button>
|
|
290
|
+
<button id="run-generative-btn">Generative Design</button>
|
|
291
|
+
<button id="run-multi-physics-btn">Multi-Physics</button>
|
|
292
|
+
<button id="run-smart-parts-btn">Smart Parts</button>
|
|
293
|
+
<button id="clear-log-btn">Clear Log</button>
|
|
294
|
+
<button class="export-btn" id="export-btn">Export JSON</button>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="test-log" id="test-log">
|
|
298
|
+
<div style="color: #64748b; font-size: 12px; padding: 8px;">
|
|
299
|
+
Click "Run All Tests" to begin...
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div class="test-footer">
|
|
304
|
+
<span>Ready</span>
|
|
305
|
+
<span class="elapsed-time" id="elapsed-time">0.0s</span>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<!-- Three.js CDN -->
|
|
310
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r170/three.min.js"></script>
|
|
311
|
+
|
|
312
|
+
<!-- Module Scripts -->
|
|
313
|
+
<script src="../js/modules/generative-design.js"></script>
|
|
314
|
+
<script src="../js/modules/multi-physics.js"></script>
|
|
315
|
+
<script src="../js/modules/smart-parts.js"></script>
|
|
316
|
+
|
|
317
|
+
<script>
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// TEST RUNNER
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
class TestRunner {
|
|
323
|
+
constructor() {
|
|
324
|
+
this.tests = [];
|
|
325
|
+
this.results = {
|
|
326
|
+
pass: 0,
|
|
327
|
+
fail: 0,
|
|
328
|
+
skip: 0,
|
|
329
|
+
total: 0,
|
|
330
|
+
details: []
|
|
331
|
+
};
|
|
332
|
+
this.startTime = null;
|
|
333
|
+
this.isRunning = false;
|
|
334
|
+
this.scene = null;
|
|
335
|
+
this.camera = null;
|
|
336
|
+
this.renderer = null;
|
|
337
|
+
this.initThreeJS();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
initThreeJS() {
|
|
341
|
+
const canvas = document.getElementById('test-canvas');
|
|
342
|
+
this.scene = new THREE.Scene();
|
|
343
|
+
this.camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
|
|
344
|
+
this.camera.position.set(0, 0, 100);
|
|
345
|
+
|
|
346
|
+
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
347
|
+
this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
|
348
|
+
this.renderer.setClearColor(0x1a202c);
|
|
349
|
+
|
|
350
|
+
window.addEventListener('resize', () => this.onWindowResize());
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
onWindowResize() {
|
|
354
|
+
const canvas = document.getElementById('test-canvas');
|
|
355
|
+
const width = canvas.clientWidth;
|
|
356
|
+
const height = canvas.clientHeight;
|
|
357
|
+
this.camera.aspect = width / height;
|
|
358
|
+
this.camera.updateProjectionMatrix();
|
|
359
|
+
this.renderer.setSize(width, height);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
addTest(category, name, fn) {
|
|
363
|
+
this.tests.push({ category, name, fn });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async runTests(filter = null) {
|
|
367
|
+
this.isRunning = true;
|
|
368
|
+
this.startTime = Date.now();
|
|
369
|
+
this.results = { pass: 0, fail: 0, skip: 0, total: 0, details: [] };
|
|
370
|
+
|
|
371
|
+
const testsToRun = filter ? this.tests.filter(t => t.category === filter) : this.tests;
|
|
372
|
+
|
|
373
|
+
for (let test of testsToRun) {
|
|
374
|
+
await this.runTest(test);
|
|
375
|
+
this.updateStats();
|
|
376
|
+
this.updateProgress(this.results.total, this.tests.length);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.isRunning = false;
|
|
380
|
+
this.logComplete();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async runTest(test) {
|
|
384
|
+
try {
|
|
385
|
+
this.log(`Running: ${test.name}`, 'running');
|
|
386
|
+
await test.fn(this.scene, this.camera, this.renderer);
|
|
387
|
+
this.results.pass++;
|
|
388
|
+
this.results.details.push({ category: test.category, name: test.name, status: 'pass' });
|
|
389
|
+
this.log(`✓ ${test.name}`, 'pass');
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this.results.fail++;
|
|
392
|
+
this.results.details.push({ category: test.category, name: test.name, status: 'fail', error: err.message });
|
|
393
|
+
this.log(`✗ ${test.name}: ${err.message}`, 'fail');
|
|
394
|
+
}
|
|
395
|
+
this.results.total++;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
log(msg, type = 'info') {
|
|
399
|
+
const logDiv = document.getElementById('test-log');
|
|
400
|
+
const entry = document.createElement('div');
|
|
401
|
+
entry.className = `test-entry ${type}`;
|
|
402
|
+
entry.innerHTML = `<div class="test-entry-msg">${msg}</div>`;
|
|
403
|
+
logDiv.appendChild(entry);
|
|
404
|
+
logDiv.scrollTop = logDiv.scrollHeight;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
logComplete() {
|
|
408
|
+
this.log(`\n✓ Test Run Complete: ${this.results.pass}/${this.results.total} passed`, 'pass');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
updateStats() {
|
|
412
|
+
document.getElementById('stat-pass').textContent = this.results.pass;
|
|
413
|
+
document.getElementById('stat-fail').textContent = this.results.fail;
|
|
414
|
+
document.getElementById('stat-skip').textContent = this.results.skip;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
updateProgress(current, total) {
|
|
418
|
+
const pct = (current / total) * 100;
|
|
419
|
+
document.getElementById('progress-fill').style.width = pct + '%';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
updateElapsed() {
|
|
423
|
+
if (this.startTime && this.isRunning) {
|
|
424
|
+
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
425
|
+
document.getElementById('elapsed-time').textContent = elapsed + 's';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
exportJSON() {
|
|
430
|
+
const json = JSON.stringify(this.results, null, 2);
|
|
431
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
432
|
+
const url = URL.createObjectURL(blob);
|
|
433
|
+
const a = document.createElement('a');
|
|
434
|
+
a.href = url;
|
|
435
|
+
a.download = `killer-features-tests-${Date.now()}.json`;
|
|
436
|
+
a.click();
|
|
437
|
+
URL.revokeObjectURL(url);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
clearLog() {
|
|
441
|
+
document.getElementById('test-log').innerHTML = '';
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const runner = new TestRunner();
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// GENERATIVE DESIGN TESTS
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
runner.addTest('Generative Design', 'Module exists with correct API', (scene) => {
|
|
452
|
+
if (!window.CycleCAD.GenerativeDesign) throw new Error('Module not found');
|
|
453
|
+
const mod = window.CycleCAD.GenerativeDesign;
|
|
454
|
+
if (!mod.init) throw new Error('Missing init()');
|
|
455
|
+
if (!mod.getUI) throw new Error('Missing getUI()');
|
|
456
|
+
if (!mod.execute) throw new Error('Missing execute()');
|
|
457
|
+
if (!mod.optimize) throw new Error('Missing optimize()');
|
|
458
|
+
if (!mod.setConstraints) throw new Error('Missing setConstraints()');
|
|
459
|
+
if (!mod.getResults) throw new Error('Missing getResults()');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
runner.addTest('Generative Design', 'getUI() returns HTMLElement', (scene) => {
|
|
463
|
+
const ui = window.CycleCAD.GenerativeDesign.getUI();
|
|
464
|
+
if (!(ui instanceof HTMLElement)) throw new Error('getUI() did not return HTMLElement');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
runner.addTest('Generative Design', 'init(scene) completes without error', (scene) => {
|
|
468
|
+
const testScene = new THREE.Scene();
|
|
469
|
+
window.CycleCAD.GenerativeDesign.init(testScene);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
runner.addTest('Generative Design', 'setConstraints() accepts keep/avoid/loads/fixed regions', (scene) => {
|
|
473
|
+
window.CycleCAD.GenerativeDesign.setConstraints({
|
|
474
|
+
keepRegions: [{ min: new THREE.Vector3(0, 0, 0), max: new THREE.Vector3(10, 10, 10) }],
|
|
475
|
+
avoidRegions: [{ min: new THREE.Vector3(-10, -10, -10), max: new THREE.Vector3(-5, -5, -5) }],
|
|
476
|
+
loads: [{ position: new THREE.Vector3(0, 20, 0), force: new THREE.Vector3(0, -100, 0) }],
|
|
477
|
+
fixedPoints: [new THREE.Vector3(0, -20, 0)]
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
runner.addTest('Generative Design', 'optimize() starts without error', (scene) => {
|
|
482
|
+
window.CycleCAD.GenerativeDesign.optimize({ maxIterations: 1 });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
runner.addTest('Generative Design', 'Default voxel grid is 20×20×20 (8000 voxels)', (scene) => {
|
|
486
|
+
const results = window.CycleCAD.GenerativeDesign.getResults();
|
|
487
|
+
// Grid should be 20^3 = 8000 voxels
|
|
488
|
+
if (results.gridResolution !== 20) throw new Error(`Expected 20, got ${results.gridResolution}`);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
runner.addTest('Generative Design', 'Volume fraction slider range 0.1-0.6', (scene) => {
|
|
492
|
+
const ui = window.CycleCAD.GenerativeDesign.getUI();
|
|
493
|
+
const slider = ui.querySelector('input[type="range"]');
|
|
494
|
+
if (!slider) throw new Error('Volume fraction slider not found');
|
|
495
|
+
if (parseFloat(slider.min) !== 0.1) throw new Error(`Min expected 0.1, got ${slider.min}`);
|
|
496
|
+
if (parseFloat(slider.max) !== 0.6) throw new Error(`Max expected 0.6, got ${slider.max}`);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
runner.addTest('Generative Design', 'getResults() returns object with expected fields', (scene) => {
|
|
500
|
+
const results = window.CycleCAD.GenerativeDesign.getResults();
|
|
501
|
+
if (!results.density) throw new Error('Missing density field');
|
|
502
|
+
if (!('compliance' in results)) throw new Error('Missing compliance field');
|
|
503
|
+
if (!('weightReduction' in results)) throw new Error('Missing weightReduction field');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
runner.addTest('Generative Design', 'execute("optimize", params) dispatches correctly', (scene) => {
|
|
507
|
+
const result = window.CycleCAD.GenerativeDesign.execute('optimize', { maxIterations: 1 });
|
|
508
|
+
if (!result) throw new Error('execute() did not return result');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
runner.addTest('Generative Design', 'Material database includes Steel, Aluminum, Titanium, ABS, Nylon', (scene) => {
|
|
512
|
+
const ui = window.CycleCAD.GenerativeDesign.getUI();
|
|
513
|
+
const materialSelect = ui.querySelector('select');
|
|
514
|
+
if (!materialSelect) throw new Error('Material select not found');
|
|
515
|
+
const options = Array.from(materialSelect.options).map(o => o.value);
|
|
516
|
+
const required = ['Steel', 'Aluminum', 'Titanium', 'ABS', 'Nylon'];
|
|
517
|
+
for (let mat of required) {
|
|
518
|
+
if (!options.includes(mat)) throw new Error(`Missing material: ${mat}`);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
runner.addTest('Generative Design', 'Marching cubes produces valid mesh from density field', (scene) => {
|
|
523
|
+
const results = window.CycleCAD.GenerativeDesign.getResults();
|
|
524
|
+
if (!results.mesh) throw new Error('Mesh not generated');
|
|
525
|
+
if (!results.mesh.geometry) throw new Error('Mesh geometry missing');
|
|
526
|
+
const geom = results.mesh.geometry;
|
|
527
|
+
if (!geom.attributes.position || geom.attributes.position.count === 0) {
|
|
528
|
+
throw new Error('Mesh has no vertices');
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
runner.addTest('Generative Design', 'STL export produces non-empty buffer/string', (scene) => {
|
|
533
|
+
const stlData = window.CycleCAD.GenerativeDesign.execute('export', { format: 'stl' });
|
|
534
|
+
if (!stlData) throw new Error('STL export returned null');
|
|
535
|
+
if (typeof stlData === 'string' && stlData.length === 0) throw new Error('STL string is empty');
|
|
536
|
+
if (stlData instanceof ArrayBuffer && stlData.byteLength === 0) throw new Error('STL buffer is empty');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// ============================================================================
|
|
540
|
+
// MULTI-PHYSICS TESTS
|
|
541
|
+
// ============================================================================
|
|
542
|
+
|
|
543
|
+
runner.addTest('Multi-Physics', 'Module exists with correct API', (scene) => {
|
|
544
|
+
if (!window.CycleCAD.MultiPhysics) throw new Error('Module not found');
|
|
545
|
+
const mod = window.CycleCAD.MultiPhysics;
|
|
546
|
+
if (!mod.init) throw new Error('Missing init()');
|
|
547
|
+
if (!mod.getUI) throw new Error('Missing getUI()');
|
|
548
|
+
if (!mod.execute) throw new Error('Missing execute()');
|
|
549
|
+
if (!mod.runSimulation) throw new Error('Missing runSimulation()');
|
|
550
|
+
if (!mod.getResults) throw new Error('Missing getResults()');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
runner.addTest('Multi-Physics', 'getUI() returns HTMLElement', (scene) => {
|
|
554
|
+
const ui = window.CycleCAD.MultiPhysics.getUI();
|
|
555
|
+
if (!(ui instanceof HTMLElement)) throw new Error('getUI() did not return HTMLElement');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
runner.addTest('Multi-Physics', 'init(scene) completes without error', (scene) => {
|
|
559
|
+
const testScene = new THREE.Scene();
|
|
560
|
+
const boxGeom = new THREE.BoxGeometry(10, 10, 10);
|
|
561
|
+
const boxMesh = new THREE.Mesh(boxGeom, new THREE.MeshStandardMaterial());
|
|
562
|
+
testScene.add(boxMesh);
|
|
563
|
+
window.CycleCAD.MultiPhysics.init(testScene);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
runner.addTest('Multi-Physics', 'Structural analysis on simple box mesh returns stress values', (scene) => {
|
|
567
|
+
const testScene = new THREE.Scene();
|
|
568
|
+
const boxGeom = new THREE.BoxGeometry(10, 10, 10);
|
|
569
|
+
const boxMesh = new THREE.Mesh(boxGeom, new THREE.MeshStandardMaterial());
|
|
570
|
+
testScene.add(boxMesh);
|
|
571
|
+
window.CycleCAD.MultiPhysics.init(testScene);
|
|
572
|
+
|
|
573
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
574
|
+
type: 'structural',
|
|
575
|
+
material: 'steel',
|
|
576
|
+
loads: [{ position: [0, 5, 0], force: [0, -1000, 0] }],
|
|
577
|
+
fixed: [[0, -5, 0]]
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
if (!result.maxStress || result.maxStress <= 0) throw new Error('No stress values returned');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
runner.addTest('Multi-Physics', 'Thermal analysis returns temperature distribution', (scene) => {
|
|
584
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
585
|
+
type: 'thermal',
|
|
586
|
+
material: 'steel',
|
|
587
|
+
heatSource: { position: [0, 0, 0], power: 100 },
|
|
588
|
+
ambientTemp: 20
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
if (!result.tempField || !Array.isArray(result.tempField)) throw new Error('No temperature field returned');
|
|
592
|
+
if (result.tempField.length === 0) throw new Error('Temperature field is empty');
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
runner.addTest('Multi-Physics', 'Modal analysis returns natural frequencies array', (scene) => {
|
|
596
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
597
|
+
type: 'modal',
|
|
598
|
+
material: 'steel',
|
|
599
|
+
numModes: 5
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
if (!result.frequencies || !Array.isArray(result.frequencies)) throw new Error('No frequencies array returned');
|
|
603
|
+
if (result.frequencies.length === 0) throw new Error('Frequencies array is empty');
|
|
604
|
+
for (let freq of result.frequencies) {
|
|
605
|
+
if (typeof freq !== 'number' || freq < 0) throw new Error('Invalid frequency value: ' + freq);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
runner.addTest('Multi-Physics', 'Drop test returns peak deceleration value', (scene) => {
|
|
610
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
611
|
+
type: 'droptest',
|
|
612
|
+
mass: 5,
|
|
613
|
+
dropHeight: 1.5,
|
|
614
|
+
material: 'steel'
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
if (!('peakDeceleration' in result)) throw new Error('Missing peakDeceleration field');
|
|
618
|
+
if (result.peakDeceleration <= 0) throw new Error('peakDeceleration should be positive');
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
runner.addTest('Multi-Physics', 'Material database has required properties', (scene) => {
|
|
622
|
+
const ui = window.CycleCAD.MultiPhysics.getUI();
|
|
623
|
+
const matSelect = ui.querySelector('select');
|
|
624
|
+
if (!matSelect) throw new Error('Material select not found');
|
|
625
|
+
|
|
626
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
627
|
+
type: 'structural',
|
|
628
|
+
material: 'aluminum',
|
|
629
|
+
loads: [],
|
|
630
|
+
fixed: []
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (!('youngsModulus' in result) && !('E' in result)) throw new Error('Missing Young\'s modulus');
|
|
634
|
+
if (!('poissonsRatio' in result) && !('nu' in result)) throw new Error('Missing Poisson\'s ratio');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
runner.addTest('Multi-Physics', 'Factor of safety calculated correctly', (scene) => {
|
|
638
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
639
|
+
type: 'structural',
|
|
640
|
+
material: 'steel',
|
|
641
|
+
loads: [{ position: [0, 5, 0], force: [0, -500, 0] }],
|
|
642
|
+
fixed: [[0, -5, 0]]
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
if (!('factorOfSafety' in result)) throw new Error('Missing factorOfSafety field');
|
|
646
|
+
if (result.factorOfSafety <= 0) throw new Error('factorOfSafety should be positive');
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
runner.addTest('Multi-Physics', 'Mesh discretization produces nodes and elements', (scene) => {
|
|
650
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
651
|
+
type: 'structural',
|
|
652
|
+
material: 'steel',
|
|
653
|
+
loads: [],
|
|
654
|
+
fixed: []
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
if (!result.nodes || result.nodes.length === 0) throw new Error('No nodes in discretization');
|
|
658
|
+
if (!result.elements || result.elements.length === 0) throw new Error('No elements in discretization');
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
runner.addTest('Multi-Physics', 'Conjugate gradient solver converges for simple system', (scene) => {
|
|
662
|
+
const result = window.CycleCAD.MultiPhysics.runSimulation({
|
|
663
|
+
type: 'structural',
|
|
664
|
+
material: 'steel',
|
|
665
|
+
solverMethod: 'cg',
|
|
666
|
+
loads: [{ position: [0, 5, 0], force: [0, -100, 0] }],
|
|
667
|
+
fixed: [[0, -5, 0]]
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (!result.converged) throw new Error('Solver did not converge');
|
|
671
|
+
if (result.iterations === undefined) throw new Error('Missing iteration count');
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
runner.addTest('Multi-Physics', 'execute("simulate", {type: "static"}) dispatches correctly', (scene) => {
|
|
675
|
+
const result = window.CycleCAD.MultiPhysics.execute('simulate', {
|
|
676
|
+
type: 'static',
|
|
677
|
+
material: 'steel',
|
|
678
|
+
loads: [],
|
|
679
|
+
fixed: []
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
if (!result) throw new Error('execute() did not return result');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// ============================================================================
|
|
686
|
+
// SMART PARTS TESTS
|
|
687
|
+
// ============================================================================
|
|
688
|
+
|
|
689
|
+
runner.addTest('Smart Parts', 'Module exists with correct API', (scene) => {
|
|
690
|
+
if (!window.CycleCAD.SmartParts) throw new Error('Module not found');
|
|
691
|
+
const mod = window.CycleCAD.SmartParts;
|
|
692
|
+
if (!mod.init) throw new Error('Missing init()');
|
|
693
|
+
if (!mod.getUI) throw new Error('Missing getUI()');
|
|
694
|
+
if (!mod.execute) throw new Error('Missing execute()');
|
|
695
|
+
if (!mod.search) throw new Error('Missing search()');
|
|
696
|
+
if (!mod.getCatalog) throw new Error('Missing getCatalog()');
|
|
697
|
+
if (!mod.insertPart) throw new Error('Missing insertPart()');
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
runner.addTest('Smart Parts', 'getUI() returns HTMLElement', (scene) => {
|
|
701
|
+
const ui = window.CycleCAD.SmartParts.getUI();
|
|
702
|
+
if (!(ui instanceof HTMLElement)) throw new Error('getUI() did not return HTMLElement');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
runner.addTest('Smart Parts', 'getCatalog() returns array with 50+ parts', (scene) => {
|
|
706
|
+
const catalog = window.CycleCAD.SmartParts.getCatalog();
|
|
707
|
+
if (!Array.isArray(catalog)) throw new Error('getCatalog() did not return array');
|
|
708
|
+
if (catalog.length < 50) throw new Error(`Expected 50+ parts, got ${catalog.length}`);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
runner.addTest('Smart Parts', 'search("M8 bolt") returns results with score > 0', (scene) => {
|
|
712
|
+
const results = window.CycleCAD.SmartParts.search('M8 bolt');
|
|
713
|
+
if (!Array.isArray(results)) throw new Error('search() did not return array');
|
|
714
|
+
if (results.length === 0) throw new Error('No results for "M8 bolt"');
|
|
715
|
+
if (typeof results[0].score !== 'number' || results[0].score <= 0) {
|
|
716
|
+
throw new Error('Result missing valid score');
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
runner.addTest('Smart Parts', 'search("bearing 10mm") finds bearings', (scene) => {
|
|
721
|
+
const results = window.CycleCAD.SmartParts.search('bearing 10mm');
|
|
722
|
+
if (results.length === 0) throw new Error('No results for "bearing 10mm"');
|
|
723
|
+
const hasBearing = results.some(r => r.category && r.category.toLowerCase().includes('bearing'));
|
|
724
|
+
if (!hasBearing) throw new Error('No bearing results found');
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
runner.addTest('Smart Parts', 'search("NEMA 17") finds stepper motors', (scene) => {
|
|
728
|
+
const results = window.CycleCAD.SmartParts.search('NEMA 17');
|
|
729
|
+
if (results.length === 0) throw new Error('No results for "NEMA 17"');
|
|
730
|
+
const hasNEMA = results.some(r => r.name && r.name.includes('NEMA'));
|
|
731
|
+
if (!hasNEMA) throw new Error('No NEMA results found');
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
runner.addTest('Smart Parts', 'search("2020 extrusion") finds aluminum profiles', (scene) => {
|
|
735
|
+
const results = window.CycleCAD.SmartParts.search('2020 extrusion');
|
|
736
|
+
if (results.length === 0) throw new Error('No results for "2020 extrusion"');
|
|
737
|
+
const hasProfile = results.some(r => r.name && r.name.includes('2020'));
|
|
738
|
+
if (!hasProfile) throw new Error('No 2020 profile results found');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
runner.addTest('Smart Parts', 'insertPart(partId, scene) adds mesh to scene', (scene) => {
|
|
742
|
+
const catalog = window.CycleCAD.SmartParts.getCatalog();
|
|
743
|
+
if (catalog.length === 0) throw new Error('No parts in catalog');
|
|
744
|
+
|
|
745
|
+
const testScene = new THREE.Scene();
|
|
746
|
+
const initialCount = testScene.children.length;
|
|
747
|
+
const partId = catalog[0].id;
|
|
748
|
+
window.CycleCAD.SmartParts.insertPart(partId, testScene);
|
|
749
|
+
|
|
750
|
+
if (testScene.children.length <= initialCount) throw new Error('Part not added to scene');
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
runner.addTest('Smart Parts', 'Geometry generators produce valid THREE.Group', (scene) => {
|
|
754
|
+
const catalog = window.CycleCAD.SmartParts.getCatalog();
|
|
755
|
+
const testScene = new THREE.Scene();
|
|
756
|
+
const partId = catalog[0].id;
|
|
757
|
+
window.CycleCAD.SmartParts.insertPart(partId, testScene);
|
|
758
|
+
|
|
759
|
+
const mesh = testScene.children[0];
|
|
760
|
+
if (!mesh) throw new Error('Mesh not created');
|
|
761
|
+
if (mesh instanceof THREE.Group) {
|
|
762
|
+
if (mesh.children.length === 0) throw new Error('Group has no children');
|
|
763
|
+
} else if (!mesh.geometry) {
|
|
764
|
+
throw new Error('Mesh has no geometry');
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
runner.addTest('Smart Parts', 'BOM export produces CSV string with headers', (scene) => {
|
|
769
|
+
const bom = window.CycleCAD.SmartParts.execute('exportBOM', {
|
|
770
|
+
format: 'csv',
|
|
771
|
+
parts: [
|
|
772
|
+
{ partId: 'fastener_shcs_m3_8', quantity: 4 },
|
|
773
|
+
{ partId: 'fastener_shcs_m3_12', quantity: 2 }
|
|
774
|
+
]
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
if (typeof bom !== 'string') throw new Error('BOM is not a string');
|
|
778
|
+
if (bom.length === 0) throw new Error('BOM is empty');
|
|
779
|
+
const lines = bom.split('\n');
|
|
780
|
+
if (lines.length < 2) throw new Error('BOM missing headers or data');
|
|
781
|
+
if (!lines[0].includes('Part')) throw new Error('BOM missing headers');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
runner.addTest('Smart Parts', 'Fuzzy search handles typos ("blet" → "bolt")', (scene) => {
|
|
785
|
+
const results = window.CycleCAD.SmartParts.search('blet');
|
|
786
|
+
// Should still find bolt-like parts due to fuzzy matching
|
|
787
|
+
if (results.length === 0) throw new Error('Fuzzy search failed for typo');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
runner.addTest('Smart Parts', 'Part prices are positive numbers', (scene) => {
|
|
791
|
+
const catalog = window.CycleCAD.SmartParts.getCatalog();
|
|
792
|
+
for (let part of catalog.slice(0, 10)) {
|
|
793
|
+
if (part.price) {
|
|
794
|
+
const price = typeof part.price === 'object' ? part.price.usd : part.price;
|
|
795
|
+
if (typeof price !== 'number' || price <= 0) {
|
|
796
|
+
throw new Error(`Invalid price for ${part.name}: ${price}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// UI EVENT HANDLERS
|
|
804
|
+
// ============================================================================
|
|
805
|
+
|
|
806
|
+
document.getElementById('run-all-btn').addEventListener('click', () => {
|
|
807
|
+
runner.clearLog();
|
|
808
|
+
runner.runTests();
|
|
809
|
+
setInterval(() => runner.updateElapsed(), 100);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
document.getElementById('run-generative-btn').addEventListener('click', () => {
|
|
813
|
+
runner.clearLog();
|
|
814
|
+
runner.runTests('Generative Design');
|
|
815
|
+
setInterval(() => runner.updateElapsed(), 100);
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
document.getElementById('run-multi-physics-btn').addEventListener('click', () => {
|
|
819
|
+
runner.clearLog();
|
|
820
|
+
runner.runTests('Multi-Physics');
|
|
821
|
+
setInterval(() => runner.updateElapsed(), 100);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
document.getElementById('run-smart-parts-btn').addEventListener('click', () => {
|
|
825
|
+
runner.clearLog();
|
|
826
|
+
runner.runTests('Smart Parts');
|
|
827
|
+
setInterval(() => runner.updateElapsed(), 100);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
document.getElementById('clear-log-btn').addEventListener('click', () => {
|
|
831
|
+
runner.clearLog();
|
|
832
|
+
runner.results = { pass: 0, fail: 0, skip: 0, total: 0, details: [] };
|
|
833
|
+
runner.updateStats();
|
|
834
|
+
runner.updateProgress(0, 36);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
document.getElementById('export-btn').addEventListener('click', () => {
|
|
838
|
+
runner.exportJSON();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// Initialize Three.js rendering loop
|
|
842
|
+
function animate() {
|
|
843
|
+
requestAnimationFrame(animate);
|
|
844
|
+
runner.renderer.render(runner.scene, runner.camera);
|
|
845
|
+
}
|
|
846
|
+
animate();
|
|
847
|
+
</script>
|
|
848
|
+
</body>
|
|
849
|
+
</html>
|