cyclecad 3.2.1 → 3.5.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.
Files changed (66) hide show
  1. package/CLAUDE.md +155 -1
  2. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  3. package/DOCKER-TESTING.md +463 -0
  4. package/FUSION360_MODULES.md +478 -0
  5. package/FUSION_MODULES_README.md +352 -0
  6. package/INTEGRATION_SNIPPETS.md +608 -0
  7. package/KILLER-FEATURES-DELIVERY.md +469 -0
  8. package/MODULES_SUMMARY.txt +337 -0
  9. package/QUICK_REFERENCE.txt +298 -0
  10. package/README-DOCKER-TESTING.txt +438 -0
  11. package/app/index.html +23 -10
  12. package/app/js/fusion-help.json +1808 -0
  13. package/app/js/help-module-v3.js +1096 -0
  14. package/app/js/killer-features-help.json +395 -0
  15. package/app/js/killer-features.js +1508 -0
  16. package/app/js/modules/fusion-assembly.js +842 -0
  17. package/app/js/modules/fusion-cam.js +785 -0
  18. package/app/js/modules/fusion-data.js +814 -0
  19. package/app/js/modules/fusion-drawing.js +844 -0
  20. package/app/js/modules/fusion-inspection.js +756 -0
  21. package/app/js/modules/fusion-render.js +774 -0
  22. package/app/js/modules/fusion-simulation.js +986 -0
  23. package/app/js/modules/fusion-sketch.js +1044 -0
  24. package/app/js/modules/fusion-solid.js +1095 -0
  25. package/app/js/modules/fusion-surface.js +949 -0
  26. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  27. package/app/tests/README.md +77 -0
  28. package/app/tests/TESTING-CHECKLIST.md +177 -0
  29. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  30. package/app/tests/brep-live-test.html +848 -0
  31. package/app/tests/docker-integration-test.html +811 -0
  32. package/app/tests/fusion-all-tests.html +670 -0
  33. package/app/tests/fusion-assembly-tests.html +461 -0
  34. package/app/tests/fusion-cam-tests.html +421 -0
  35. package/app/tests/fusion-simulation-tests.html +421 -0
  36. package/app/tests/fusion-sketch-tests.html +613 -0
  37. package/app/tests/fusion-solid-tests.html +529 -0
  38. package/app/tests/index.html +453 -0
  39. package/app/tests/killer-features-test.html +509 -0
  40. package/app/tests/run-tests.html +874 -0
  41. package/app/tests/step-import-live-test.html +1115 -0
  42. package/app/tests/test-agent-v3.html +93 -696
  43. package/architecture-dashboard.html +1970 -0
  44. package/docs/API-REFERENCE.md +1423 -0
  45. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  46. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  47. package/docs/DOCKER-QUICK-TEST.md +376 -0
  48. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  49. package/docs/FUSION-TUTORIAL.md +1203 -0
  50. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  51. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  52. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  53. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  54. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  55. package/docs/KILLER-FEATURES.md +562 -0
  56. package/docs/QUICK-REFERENCE.md +282 -0
  57. package/docs/README-v3-DOCS.md +274 -0
  58. package/docs/TUTORIAL-v3.md +1190 -0
  59. package/docs/architecture-dashboard.html +1970 -0
  60. package/docs/architecture-v3.html +1038 -0
  61. package/linkedin-post-v3.md +58 -0
  62. package/package.json +1 -1
  63. package/scripts/dev-setup.sh +338 -0
  64. package/scripts/docker-health-check.sh +159 -0
  65. package/scripts/integration-test.sh +311 -0
  66. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,1115 @@
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>STEP Import Live Test Suite</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ body {
11
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
+ background: #1e1e1e;
13
+ color: #e0e0e0;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .header {
18
+ background: #2d2d30;
19
+ border-bottom: 1px solid #3e3e42;
20
+ padding: 16px 20px;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ flex-wrap: wrap;
25
+ gap: 16px;
26
+ }
27
+
28
+ .header h1 {
29
+ font-size: 20px;
30
+ font-weight: 600;
31
+ }
32
+
33
+ .header-right {
34
+ display: flex;
35
+ gap: 12px;
36
+ align-items: center;
37
+ flex-wrap: wrap;
38
+ }
39
+
40
+ .container {
41
+ display: grid;
42
+ grid-template-columns: 1fr 1fr;
43
+ gap: 0;
44
+ height: calc(100vh - 60px);
45
+ overflow: hidden;
46
+ }
47
+
48
+ .left-panel {
49
+ display: flex;
50
+ flex-direction: column;
51
+ border-right: 1px solid #3e3e42;
52
+ overflow: hidden;
53
+ }
54
+
55
+ .controls-section {
56
+ padding: 16px;
57
+ background: #252526;
58
+ border-bottom: 1px solid #3e3e42;
59
+ flex-shrink: 0;
60
+ }
61
+
62
+ .control-group {
63
+ margin-bottom: 16px;
64
+ }
65
+
66
+ .control-group:last-child {
67
+ margin-bottom: 0;
68
+ }
69
+
70
+ .label {
71
+ display: block;
72
+ font-size: 12px;
73
+ text-transform: uppercase;
74
+ letter-spacing: 0.5px;
75
+ margin-bottom: 6px;
76
+ color: #cccccc;
77
+ font-weight: 600;
78
+ }
79
+
80
+ .file-picker {
81
+ display: flex;
82
+ gap: 8px;
83
+ }
84
+
85
+ .file-input-wrapper {
86
+ position: relative;
87
+ overflow: hidden;
88
+ flex: 1;
89
+ }
90
+
91
+ .file-input-wrapper input {
92
+ display: none;
93
+ }
94
+
95
+ .file-input-label {
96
+ display: block;
97
+ padding: 8px 12px;
98
+ background: #0e639c;
99
+ color: white;
100
+ border-radius: 4px;
101
+ cursor: pointer;
102
+ text-align: center;
103
+ font-size: 13px;
104
+ font-weight: 500;
105
+ transition: background 0.2s;
106
+ }
107
+
108
+ .file-input-label:hover {
109
+ background: #1177bb;
110
+ }
111
+
112
+ .strategy-selector {
113
+ display: grid;
114
+ grid-template-columns: 1fr 1fr;
115
+ gap: 8px;
116
+ }
117
+
118
+ .strategy-btn {
119
+ padding: 8px 12px;
120
+ background: #3e3e42;
121
+ border: 1px solid #3e3e42;
122
+ color: #e0e0e0;
123
+ border-radius: 4px;
124
+ cursor: pointer;
125
+ font-size: 12px;
126
+ font-weight: 500;
127
+ transition: all 0.2s;
128
+ }
129
+
130
+ .strategy-btn:hover {
131
+ background: #4e4e54;
132
+ border-color: #007acc;
133
+ }
134
+
135
+ .strategy-btn.active {
136
+ background: #007acc;
137
+ border-color: #007acc;
138
+ color: white;
139
+ }
140
+
141
+ .progress-container {
142
+ padding: 16px;
143
+ background: #252526;
144
+ border-bottom: 1px solid #3e3e42;
145
+ flex-shrink: 0;
146
+ }
147
+
148
+ .progress-bar {
149
+ width: 100%;
150
+ height: 6px;
151
+ background: #3e3e42;
152
+ border-radius: 3px;
153
+ overflow: hidden;
154
+ margin-bottom: 8px;
155
+ }
156
+
157
+ .progress-fill {
158
+ height: 100%;
159
+ background: #007acc;
160
+ width: 0%;
161
+ transition: width 0.3s;
162
+ }
163
+
164
+ .progress-text {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ font-size: 12px;
168
+ color: #999;
169
+ }
170
+
171
+ .action-buttons {
172
+ display: flex;
173
+ gap: 8px;
174
+ margin-top: 12px;
175
+ }
176
+
177
+ button {
178
+ padding: 8px 12px;
179
+ background: #0e639c;
180
+ color: white;
181
+ border: none;
182
+ border-radius: 4px;
183
+ cursor: pointer;
184
+ font-size: 13px;
185
+ font-weight: 500;
186
+ transition: background 0.2s;
187
+ }
188
+
189
+ button:hover:not(:disabled) {
190
+ background: #1177bb;
191
+ }
192
+
193
+ button:disabled {
194
+ background: #3e3e42;
195
+ color: #999;
196
+ cursor: not-allowed;
197
+ }
198
+
199
+ .danger {
200
+ background: #d13438;
201
+ }
202
+
203
+ .danger:hover:not(:disabled) {
204
+ background: #e81123;
205
+ }
206
+
207
+ .log-panel {
208
+ flex: 1;
209
+ overflow-y: auto;
210
+ padding: 12px 16px;
211
+ background: #1e1e1e;
212
+ font-family: 'Courier New', monospace;
213
+ font-size: 12px;
214
+ line-height: 1.5;
215
+ }
216
+
217
+ .log-entry {
218
+ padding: 4px 0;
219
+ border-bottom: 1px solid #2d2d30;
220
+ }
221
+
222
+ .log-entry:last-child {
223
+ border-bottom: none;
224
+ }
225
+
226
+ .log-time {
227
+ color: #858585;
228
+ margin-right: 8px;
229
+ }
230
+
231
+ .log-info {
232
+ color: #4fc1ff;
233
+ }
234
+
235
+ .log-success {
236
+ color: #89d185;
237
+ }
238
+
239
+ .log-error {
240
+ color: #f48771;
241
+ }
242
+
243
+ .log-warn {
244
+ color: #dcdcaa;
245
+ }
246
+
247
+ .right-panel {
248
+ display: flex;
249
+ flex-direction: column;
250
+ background: #252526;
251
+ }
252
+
253
+ .viewport {
254
+ flex: 1;
255
+ position: relative;
256
+ overflow: hidden;
257
+ }
258
+
259
+ #viewport {
260
+ width: 100%;
261
+ height: 100%;
262
+ }
263
+
264
+ .stats-panel {
265
+ background: #1e1e1e;
266
+ border-top: 1px solid #3e3e42;
267
+ padding: 12px 16px;
268
+ max-height: 200px;
269
+ overflow-y: auto;
270
+ display: grid;
271
+ grid-template-columns: 1fr 1fr;
272
+ gap: 8px;
273
+ font-size: 12px;
274
+ }
275
+
276
+ .stat-item {
277
+ background: #2d2d30;
278
+ padding: 8px;
279
+ border-radius: 4px;
280
+ border-left: 3px solid #007acc;
281
+ }
282
+
283
+ .stat-label {
284
+ color: #999;
285
+ font-size: 11px;
286
+ text-transform: uppercase;
287
+ letter-spacing: 0.5px;
288
+ margin-bottom: 2px;
289
+ }
290
+
291
+ .stat-value {
292
+ color: #e0e0e0;
293
+ font-weight: 600;
294
+ font-family: 'Courier New', monospace;
295
+ }
296
+
297
+ .part-list {
298
+ max-height: 150px;
299
+ overflow-y: auto;
300
+ padding: 8px;
301
+ background: #1e1e1e;
302
+ border-top: 1px solid #3e3e42;
303
+ }
304
+
305
+ .part-item {
306
+ padding: 4px 8px;
307
+ font-size: 11px;
308
+ color: #999;
309
+ cursor: pointer;
310
+ border-radius: 3px;
311
+ transition: background 0.2s;
312
+ }
313
+
314
+ .part-item:hover {
315
+ background: #2d2d30;
316
+ color: #e0e0e0;
317
+ }
318
+
319
+ .part-item.selected {
320
+ background: #007acc;
321
+ color: white;
322
+ }
323
+
324
+ .status-badge {
325
+ display: inline-block;
326
+ padding: 2px 6px;
327
+ border-radius: 3px;
328
+ font-size: 11px;
329
+ font-weight: 600;
330
+ text-transform: uppercase;
331
+ letter-spacing: 0.5px;
332
+ margin-left: 8px;
333
+ }
334
+
335
+ .status-idle {
336
+ background: #3e3e42;
337
+ color: #999;
338
+ }
339
+
340
+ .status-loading {
341
+ background: #007acc;
342
+ color: white;
343
+ }
344
+
345
+ .status-success {
346
+ background: #89d185;
347
+ color: #000;
348
+ }
349
+
350
+ .status-error {
351
+ background: #d13438;
352
+ color: white;
353
+ }
354
+
355
+ .test-case {
356
+ background: #2d2d30;
357
+ padding: 8px 12px;
358
+ border-radius: 4px;
359
+ margin-bottom: 8px;
360
+ font-size: 12px;
361
+ }
362
+
363
+ .test-case-header {
364
+ font-weight: 600;
365
+ margin-bottom: 4px;
366
+ color: #e0e0e0;
367
+ }
368
+
369
+ .test-case-desc {
370
+ color: #999;
371
+ font-size: 11px;
372
+ margin-bottom: 4px;
373
+ }
374
+
375
+ .test-case-buttons {
376
+ display: flex;
377
+ gap: 4px;
378
+ }
379
+
380
+ .test-case-buttons button {
381
+ padding: 4px 8px;
382
+ font-size: 11px;
383
+ flex: 1;
384
+ }
385
+
386
+ ::-webkit-scrollbar {
387
+ width: 10px;
388
+ height: 10px;
389
+ }
390
+
391
+ ::-webkit-scrollbar-track {
392
+ background: #1e1e1e;
393
+ }
394
+
395
+ ::-webkit-scrollbar-thumb {
396
+ background: #3e3e42;
397
+ border-radius: 5px;
398
+ }
399
+
400
+ ::-webkit-scrollbar-thumb:hover {
401
+ background: #4e4e54;
402
+ }
403
+
404
+ .hidden {
405
+ display: none;
406
+ }
407
+ </style>
408
+ </head>
409
+ <body>
410
+ <div class="header">
411
+ <h1>STEP Import Live Test Suite</h1>
412
+ <div class="header-right">
413
+ <span class="status-badge status-idle" id="statusBadge">Idle</span>
414
+ <button id="exportBtn" disabled>Export Results</button>
415
+ </div>
416
+ </div>
417
+
418
+ <div class="container">
419
+ <div class="left-panel">
420
+ <div class="controls-section">
421
+ <div class="control-group">
422
+ <label class="label">📁 File Input</label>
423
+ <div class="file-picker">
424
+ <div class="file-input-wrapper">
425
+ <input type="file" id="fileInput" accept=".step,.stp,.STEP,.STP">
426
+ <label class="file-input-label" for="fileInput">Choose File</label>
427
+ </div>
428
+ <button id="clearBtn">Clear</button>
429
+ </div>
430
+ <div id="fileName" style="margin-top: 8px; color: #999; font-size: 12px;">No file selected</div>
431
+ </div>
432
+
433
+ <div class="control-group">
434
+ <label class="label">🔄 Strategy</label>
435
+ <div class="strategy-selector">
436
+ <button class="strategy-btn active" data-strategy="auto">Auto Detect</button>
437
+ <button class="strategy-btn" data-strategy="server">Server</button>
438
+ <button class="strategy-btn" data-strategy="opencascade">OCC WASM</button>
439
+ <button class="strategy-btn" data-strategy="occt">occt-import-js</button>
440
+ </div>
441
+ </div>
442
+
443
+ <div class="progress-container hidden" id="progressContainer">
444
+ <label class="label">Progress</label>
445
+ <div class="progress-bar">
446
+ <div class="progress-fill" id="progressFill"></div>
447
+ </div>
448
+ <div class="progress-text">
449
+ <span id="progressPercent">0%</span>
450
+ <span id="progressTime">0s</span>
451
+ </div>
452
+ <div class="action-buttons">
453
+ <button id="importBtn" disabled>Import File</button>
454
+ <button id="cancelBtn" class="danger hidden">Cancel</button>
455
+ </div>
456
+ </div>
457
+
458
+ <div class="progress-container hidden" id="idleContainer">
459
+ <div class="action-buttons">
460
+ <button id="importBtn2">Import File</button>
461
+ </div>
462
+ </div>
463
+ </div>
464
+
465
+ <div class="log-panel" id="logPanel"></div>
466
+ </div>
467
+
468
+ <div class="right-panel">
469
+ <div class="viewport">
470
+ <div id="viewport"></div>
471
+ </div>
472
+
473
+ <div class="stats-panel" id="statsPanel">
474
+ <div class="stat-item">
475
+ <div class="stat-label">Parts</div>
476
+ <div class="stat-value" id="statParts">0</div>
477
+ </div>
478
+ <div class="stat-item">
479
+ <div class="stat-label">Vertices</div>
480
+ <div class="stat-value" id="statVertices">0</div>
481
+ </div>
482
+ <div class="stat-item">
483
+ <div class="stat-label">Faces</div>
484
+ <div class="stat-value" id="statFaces">0</div>
485
+ </div>
486
+ <div class="stat-item">
487
+ <div class="stat-label">File Size</div>
488
+ <div class="stat-value" id="statFileSize">0 B</div>
489
+ </div>
490
+ <div class="stat-item">
491
+ <div class="stat-label">Parse Time</div>
492
+ <div class="stat-value" id="statParseTime">0 ms</div>
493
+ </div>
494
+ <div class="stat-item">
495
+ <div class="stat-label">Strategy Used</div>
496
+ <div class="stat-value" id="statStrategy">—</div>
497
+ </div>
498
+ </div>
499
+
500
+ <div class="part-list" id="partList"></div>
501
+ </div>
502
+ </div>
503
+
504
+ <script type="importmap">
505
+ {
506
+ "imports": {
507
+ "three": "https://cdn.jsdelivr.net/npm/three@r170/build/three.module.js",
508
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@r170/examples/jsm/"
509
+ }
510
+ }
511
+ </script>
512
+
513
+ <script type="module">
514
+ import * as THREE from 'three';
515
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
516
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
517
+
518
+ // ========== STATE ==========
519
+ let scene, camera, renderer, controls;
520
+ let selectedFile = null;
521
+ let isImporting = false;
522
+ let importStartTime = 0;
523
+ let currentStrategy = 'auto';
524
+ let importedParts = [];
525
+ let selectedPartIndex = -1;
526
+
527
+ // ========== INITIALIZATION ==========
528
+ function initViewport() {
529
+ const container = document.getElementById('viewport');
530
+
531
+ scene = new THREE.Scene();
532
+ scene.background = new THREE.Color(0x1e1e1e);
533
+ scene.fog = new THREE.Fog(0x1e1e1e, 1000, 10000);
534
+
535
+ camera = new THREE.PerspectiveCamera(
536
+ 75,
537
+ container.clientWidth / container.clientHeight,
538
+ 0.1,
539
+ 10000
540
+ );
541
+ camera.position.set(500, 500, 500);
542
+ camera.lookAt(0, 0, 0);
543
+
544
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
545
+ renderer.setSize(container.clientWidth, container.clientHeight);
546
+ renderer.shadowMap.enabled = true;
547
+ renderer.shadowMap.type = THREE.PCFShadowShadowMap;
548
+ renderer.setPixelRatio(window.devicePixelRatio);
549
+ container.appendChild(renderer.domElement);
550
+
551
+ // Lights
552
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
553
+ scene.add(ambientLight);
554
+
555
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
556
+ directionalLight.position.set(500, 500, 500);
557
+ directionalLight.castShadow = true;
558
+ directionalLight.shadow.mapSize.width = 2048;
559
+ directionalLight.shadow.mapSize.height = 2048;
560
+ directionalLight.shadow.camera.far = 2000;
561
+ scene.add(directionalLight);
562
+
563
+ // Grid
564
+ const gridHelper = new THREE.GridHelper(1000, 20, 0x444444, 0x222222);
565
+ scene.add(gridHelper);
566
+
567
+ // Controls
568
+ controls = new OrbitControls(camera, renderer.domElement);
569
+ controls.autoRotate = false;
570
+ controls.enableDamping = true;
571
+ controls.dampingFactor = 0.05;
572
+
573
+ // Handle resize
574
+ window.addEventListener('resize', () => {
575
+ const w = container.clientWidth;
576
+ const h = container.clientHeight;
577
+ camera.aspect = w / h;
578
+ camera.updateProjectionMatrix();
579
+ renderer.setSize(w, h);
580
+ });
581
+
582
+ animate();
583
+ }
584
+
585
+ function animate() {
586
+ requestAnimationFrame(animate);
587
+ controls.update();
588
+ renderer.render(scene, camera);
589
+ }
590
+
591
+ // ========== LOGGING ==========
592
+ function log(message, level = 'info') {
593
+ const logPanel = document.getElementById('logPanel');
594
+ const entry = document.createElement('div');
595
+ entry.className = 'log-entry';
596
+
597
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
598
+ const levelClass = `log-${level}`;
599
+
600
+ entry.innerHTML = `
601
+ <span class="log-time">[${time}]</span>
602
+ <span class="${levelClass}">${message}</span>
603
+ `;
604
+
605
+ logPanel.appendChild(entry);
606
+ logPanel.scrollTop = logPanel.scrollHeight;
607
+ }
608
+
609
+ // ========== PROGRESS ==========
610
+ function setProgress(percent, elapsed = 0) {
611
+ document.getElementById('progressFill').style.width = percent + '%';
612
+ document.getElementById('progressPercent').textContent = percent + '%';
613
+ document.getElementById('progressTime').textContent = elapsed + 's';
614
+ }
615
+
616
+ function updateStatus(status, text) {
617
+ const badge = document.getElementById('statusBadge');
618
+ badge.className = 'status-badge status-' + status;
619
+ badge.textContent = text.toUpperCase();
620
+ }
621
+
622
+ // ========== FILE HANDLING ==========
623
+ document.getElementById('fileInput').addEventListener('change', (e) => {
624
+ const file = e.target.files[0];
625
+ if (!file) return;
626
+
627
+ selectedFile = file;
628
+ document.getElementById('fileName').textContent = `📄 ${file.name} (${formatBytes(file.size)})`;
629
+ log(`File selected: ${file.name}`, 'success');
630
+
631
+ document.getElementById('progressContainer').classList.remove('hidden');
632
+ document.getElementById('idleContainer').classList.add('hidden');
633
+ });
634
+
635
+ document.getElementById('clearBtn').addEventListener('click', () => {
636
+ selectedFile = null;
637
+ document.getElementById('fileInput').value = '';
638
+ document.getElementById('fileName').textContent = 'No file selected';
639
+ document.getElementById('progressContainer').classList.add('hidden');
640
+ document.getElementById('idleContainer').classList.remove('hidden');
641
+ log('File cleared', 'info');
642
+ });
643
+
644
+ // Strategy selection
645
+ document.querySelectorAll('.strategy-btn').forEach(btn => {
646
+ btn.addEventListener('click', () => {
647
+ document.querySelectorAll('.strategy-btn').forEach(b => b.classList.remove('active'));
648
+ btn.classList.add('active');
649
+ currentStrategy = btn.dataset.strategy;
650
+ log(`Strategy selected: ${currentStrategy}`, 'info');
651
+ });
652
+ });
653
+
654
+ // ========== IMPORT ==========
655
+ async function importFile() {
656
+ if (!selectedFile) {
657
+ log('No file selected', 'error');
658
+ return;
659
+ }
660
+
661
+ isImporting = true;
662
+ importStartTime = Date.now();
663
+ updateStatus('loading', 'Importing');
664
+ document.getElementById('importBtn').disabled = true;
665
+ document.getElementById('cancelBtn').classList.remove('hidden');
666
+
667
+ log(`Starting import with strategy: ${currentStrategy}`, 'info');
668
+ log(`File size: ${formatBytes(selectedFile.size)}`, 'info');
669
+
670
+ try {
671
+ const strategy = currentStrategy === 'auto'
672
+ ? selectedFile.size > 50 * 1024 * 1024 ? 'server' : 'opencascade'
673
+ : currentStrategy;
674
+
675
+ log(`Using strategy: ${strategy}`, 'warn');
676
+
677
+ switch (strategy) {
678
+ case 'server':
679
+ await importViaServer();
680
+ break;
681
+ case 'opencascade':
682
+ await importViaOpenCascade();
683
+ break;
684
+ case 'occt':
685
+ await importViaOCCT();
686
+ break;
687
+ }
688
+
689
+ updateStatus('success', 'Import Complete');
690
+ log('Import completed successfully', 'success');
691
+ } catch (err) {
692
+ updateStatus('error', 'Import Failed');
693
+ log(`Error: ${err.message}`, 'error');
694
+ } finally {
695
+ isImporting = false;
696
+ document.getElementById('importBtn').disabled = false;
697
+ document.getElementById('cancelBtn').classList.add('hidden');
698
+ }
699
+ }
700
+
701
+ async function importViaServer() {
702
+ log('Uploading file to server (http://localhost:8787/convert)...', 'info');
703
+
704
+ const formData = new FormData();
705
+ formData.append('file', selectedFile);
706
+
707
+ const startTime = Date.now();
708
+ let uploadedBytes = 0;
709
+
710
+ try {
711
+ const xhr = new XMLHttpRequest();
712
+
713
+ xhr.upload.addEventListener('progress', (e) => {
714
+ if (e.lengthComputable) {
715
+ const percent = Math.round((e.loaded / e.total) * 100);
716
+ setProgress(percent, Math.round((Date.now() - startTime) / 1000));
717
+ uploadedBytes = e.loaded;
718
+ }
719
+ });
720
+
721
+ const response = await new Promise((resolve, reject) => {
722
+ xhr.addEventListener('load', () => {
723
+ if (xhr.status === 200) {
724
+ resolve(xhr.response);
725
+ } else {
726
+ reject(new Error(`Server error: ${xhr.status}`));
727
+ }
728
+ });
729
+ xhr.addEventListener('error', () => reject(new Error('Network error')));
730
+ xhr.open('POST', 'http://localhost:8787/convert');
731
+ xhr.responseType = 'arraybuffer';
732
+ xhr.send(formData);
733
+ });
734
+
735
+ log(`Upload complete. Received ${formatBytes(response.byteLength)} GLB`, 'success');
736
+
737
+ const parseStart = Date.now();
738
+ const glb = new Uint8Array(response);
739
+ const blob = new Blob([glb], { type: 'model/gltf-binary' });
740
+ const url = URL.createObjectURL(blob);
741
+
742
+ const loader = new GLTFLoader();
743
+ const gltf = await new Promise((resolve, reject) => {
744
+ loader.load(url, resolve, undefined, reject);
745
+ });
746
+
747
+ const parseTime = Date.now() - parseStart;
748
+ log(`GLB loaded in ${parseTime}ms`, 'success');
749
+
750
+ // Add to scene
751
+ const group = gltf.scene;
752
+ scene.add(group);
753
+
754
+ // Fit camera
755
+ const box = new THREE.Box3().setFromObject(group);
756
+ const size = box.getSize(new THREE.Vector3());
757
+ const maxDim = Math.max(size.x, size.y, size.z);
758
+ const fov = camera.fov * (Math.PI / 180);
759
+ let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
760
+ cameraZ *= 1.5;
761
+
762
+ camera.position.z = cameraZ;
763
+ camera.lookAt(box.getCenter(new THREE.Vector3()));
764
+ controls.target.copy(box.getCenter(new THREE.Vector3()));
765
+ controls.update();
766
+
767
+ // Analyze geometry
768
+ analyzeGeometry(group);
769
+
770
+ const totalTime = Date.now() - startTime;
771
+ document.getElementById('statParseTime').textContent = totalTime + ' ms';
772
+ document.getElementById('statStrategy').textContent = 'Server';
773
+
774
+ setProgress(100, Math.round(totalTime / 1000));
775
+ URL.revokeObjectURL(url);
776
+ } catch (err) {
777
+ throw new Error(`Server import failed: ${err.message}`);
778
+ }
779
+ }
780
+
781
+ async function importViaOpenCascade() {
782
+ log('Loading OpenCascade.js WASM from CDN...', 'info');
783
+
784
+ try {
785
+ // Load OpenCascade.js
786
+ const CDN_BASE = 'https://cdn.jsdelivr.net/npm/opencascade.js@2.0.0-beta.b5ff984/dist/';
787
+
788
+ // Load the JS file
789
+ const script = document.createElement('script');
790
+ script.src = CDN_BASE + 'opencascade.full.js';
791
+
792
+ const scriptLoaded = new Promise((resolve, reject) => {
793
+ script.onload = resolve;
794
+ script.onerror = reject;
795
+ });
796
+
797
+ document.head.appendChild(script);
798
+ await scriptLoaded;
799
+
800
+ log('OpenCascade.js loaded', 'success');
801
+
802
+ // Initialize OCC with locateFile
803
+ const oc = await new Module({
804
+ locateFile: (file) => CDN_BASE + file
805
+ });
806
+
807
+ log('WASM initialized', 'success');
808
+
809
+ // Parse STEP file
810
+ const arrayBuffer = await selectedFile.arrayBuffer();
811
+ const uint8Array = new Uint8Array(arrayBuffer);
812
+
813
+ const parseStart = Date.now();
814
+ log('Parsing STEP file...', 'info');
815
+
816
+ // Use STEPControl_Reader
817
+ const reader = new oc.STEPControl_Reader();
818
+ const status = reader.ReadFile(uint8Array);
819
+
820
+ if (status !== 0) {
821
+ throw new Error(`ReadFile failed with status ${status}`);
822
+ }
823
+
824
+ reader.TransferRoots();
825
+ const shape = reader.OneShape();
826
+
827
+ log('STEP parsed, tessellating...', 'info');
828
+
829
+ // Tessellate
830
+ oc.BRepMesh_IncrementalMesh(shape, 0.1);
831
+
832
+ // Convert to Three.js
833
+ const group = new THREE.Group();
834
+ const faces = oc.TopExp_Explorer(shape, oc.TopAbs_FACE);
835
+
836
+ let partCount = 0;
837
+ while (faces.More()) {
838
+ const face = faces.Current();
839
+ const triangulation = oc.BRepLProp_CLProps(face, 2, 1e-7);
840
+
841
+ // Create mesh
842
+ const geometry = new THREE.BufferGeometry();
843
+ const material = new THREE.MeshStandardMaterial({
844
+ color: 0x007acc,
845
+ metalness: 0.3,
846
+ roughness: 0.7
847
+ });
848
+ const mesh = new THREE.Mesh(geometry, material);
849
+ mesh.castShadow = true;
850
+ mesh.receiveShadow = true;
851
+
852
+ group.add(mesh);
853
+ importedParts.push({ name: `Part_${partCount}`, mesh });
854
+ partCount++;
855
+
856
+ faces.Next();
857
+ }
858
+
859
+ const parseTime = Date.now() - parseStart;
860
+ log(`Tessellated into ${partCount} parts in ${parseTime}ms`, 'success');
861
+
862
+ scene.add(group);
863
+
864
+ // Fit camera
865
+ const box = new THREE.Box3().setFromObject(group);
866
+ const size = box.getSize(new THREE.Vector3());
867
+ const maxDim = Math.max(size.x, size.y, size.z);
868
+ const fov = camera.fov * (Math.PI / 180);
869
+ let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
870
+ cameraZ *= 1.5;
871
+
872
+ camera.position.z = cameraZ;
873
+ camera.lookAt(box.getCenter(new THREE.Vector3()));
874
+ controls.target.copy(box.getCenter(new THREE.Vector3()));
875
+ controls.update();
876
+
877
+ // Analyze
878
+ analyzeGeometry(group);
879
+
880
+ const totalTime = Date.now() - importStartTime;
881
+ document.getElementById('statParseTime').textContent = totalTime + ' ms';
882
+ document.getElementById('statStrategy').textContent = 'OpenCascade.js';
883
+
884
+ setProgress(100, Math.round(totalTime / 1000));
885
+ } catch (err) {
886
+ throw new Error(`OpenCascade.js import failed: ${err.message}`);
887
+ }
888
+ }
889
+
890
+ async function importViaOCCT() {
891
+ log('Loading occt-import-js from CDN...', 'info');
892
+
893
+ try {
894
+ // Load library
895
+ const script = document.createElement('script');
896
+ script.src = 'https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/occt-import.umd.js';
897
+
898
+ const scriptLoaded = new Promise((resolve, reject) => {
899
+ script.onload = resolve;
900
+ script.onerror = reject;
901
+ });
902
+
903
+ document.head.appendChild(script);
904
+ await scriptLoaded;
905
+
906
+ log('occt-import-js loaded, starting parse...', 'success');
907
+
908
+ const parseStart = Date.now();
909
+ const arrayBuffer = await selectedFile.arrayBuffer();
910
+
911
+ // Create worker for parsing
912
+ const workerCode = `
913
+ self.importScripts('https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/occt-import.umd.js');
914
+ self.onmessage = async (e) => {
915
+ try {
916
+ const result = window.occtImport(new Uint8Array(e.data));
917
+ const meshes = result.meshes.map(m => ({
918
+ name: m.name,
919
+ positions: Array.from(m.attributes.position.array),
920
+ indices: Array.from(m.indices)
921
+ }));
922
+ self.postMessage({ success: true, meshes });
923
+ } catch (err) {
924
+ self.postMessage({ success: false, error: err.message });
925
+ }
926
+ };
927
+ `;
928
+
929
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
930
+ const workerUrl = URL.createObjectURL(blob);
931
+ const worker = new Worker(workerUrl);
932
+
933
+ const result = await new Promise((resolve, reject) => {
934
+ const timeout = setTimeout(() => {
935
+ worker.terminate();
936
+ reject(new Error('Worker timeout after 90s'));
937
+ }, 90000);
938
+
939
+ worker.onmessage = (e) => {
940
+ clearTimeout(timeout);
941
+ if (e.data.success) {
942
+ resolve(e.data.meshes);
943
+ } else {
944
+ reject(new Error(e.data.error));
945
+ }
946
+ };
947
+
948
+ worker.onerror = (err) => {
949
+ clearTimeout(timeout);
950
+ reject(err);
951
+ };
952
+
953
+ worker.postMessage(arrayBuffer, [arrayBuffer]);
954
+ });
955
+
956
+ const parseTime = Date.now() - parseStart;
957
+ log(`Parsed ${result.length} meshes in ${parseTime}ms`, 'success');
958
+
959
+ // Add to scene
960
+ const group = new THREE.Group();
961
+ let vertexCount = 0;
962
+
963
+ result.forEach((meshData, idx) => {
964
+ const geometry = new THREE.BufferGeometry();
965
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(meshData.positions), 3));
966
+
967
+ if (meshData.indices && meshData.indices.length > 0) {
968
+ geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(meshData.indices), 1));
969
+ }
970
+
971
+ geometry.computeVertexNormals();
972
+ vertexCount += meshData.positions.length / 3;
973
+
974
+ const material = new THREE.MeshStandardMaterial({
975
+ color: Math.random() * 0xffffff,
976
+ metalness: 0.3,
977
+ roughness: 0.7
978
+ });
979
+
980
+ const mesh = new THREE.Mesh(geometry, material);
981
+ mesh.castShadow = true;
982
+ mesh.receiveShadow = true;
983
+
984
+ group.add(mesh);
985
+ importedParts.push({ name: meshData.name || `Mesh_${idx}`, mesh });
986
+ });
987
+
988
+ log(`Total vertices: ${vertexCount}`, 'success');
989
+
990
+ scene.add(group);
991
+
992
+ // Fit camera
993
+ const box = new THREE.Box3().setFromObject(group);
994
+ const size = box.getSize(new THREE.Vector3());
995
+ const maxDim = Math.max(size.x, size.y, size.z);
996
+ const fov = camera.fov * (Math.PI / 180);
997
+ let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
998
+ cameraZ *= 1.5;
999
+
1000
+ camera.position.z = cameraZ;
1001
+ camera.lookAt(box.getCenter(new THREE.Vector3()));
1002
+ controls.target.copy(box.getCenter(new THREE.Vector3()));
1003
+ controls.update();
1004
+
1005
+ // Analyze
1006
+ analyzeGeometry(group);
1007
+
1008
+ const totalTime = Date.now() - importStartTime;
1009
+ document.getElementById('statParseTime').textContent = totalTime + ' ms';
1010
+ document.getElementById('statStrategy').textContent = 'occt-import-js';
1011
+
1012
+ setProgress(100, Math.round(totalTime / 1000));
1013
+
1014
+ worker.terminate();
1015
+ URL.revokeObjectURL(workerUrl);
1016
+ } catch (err) {
1017
+ throw new Error(`occt-import-js import failed: ${err.message}`);
1018
+ }
1019
+ }
1020
+
1021
+ function analyzeGeometry(group) {
1022
+ let vertices = 0;
1023
+ let faces = 0;
1024
+ const partList = document.getElementById('partList');
1025
+ partList.innerHTML = '';
1026
+
1027
+ group.traverse((node) => {
1028
+ if (node.isMesh && node.geometry) {
1029
+ const posAttr = node.geometry.attributes.position;
1030
+ if (posAttr) vertices += posAttr.count;
1031
+
1032
+ if (node.geometry.index) {
1033
+ faces += node.geometry.index.count / 3;
1034
+ }
1035
+
1036
+ const name = node.name || `Mesh_${importedParts.length}`;
1037
+ const item = document.createElement('div');
1038
+ item.className = 'part-item';
1039
+ item.textContent = name;
1040
+ item.addEventListener('click', () => {
1041
+ document.querySelectorAll('.part-item').forEach(p => p.classList.remove('selected'));
1042
+ item.classList.add('selected');
1043
+
1044
+ // Highlight mesh
1045
+ group.traverse(child => {
1046
+ if (child.isMesh) {
1047
+ child.material.emissive.set(0x000000);
1048
+ }
1049
+ });
1050
+ node.material.emissive.set(0x00ff00);
1051
+ selectedPartIndex = importedParts.findIndex(p => p.mesh === node);
1052
+ });
1053
+ partList.appendChild(item);
1054
+ }
1055
+ });
1056
+
1057
+ document.getElementById('statParts').textContent = importedParts.length;
1058
+ document.getElementById('statVertices').textContent = vertices.toLocaleString();
1059
+ document.getElementById('statFaces').textContent = Math.round(faces).toLocaleString();
1060
+ document.getElementById('statFileSize').textContent = formatBytes(selectedFile.size);
1061
+ }
1062
+
1063
+ // ========== UTILITIES ==========
1064
+ function formatBytes(bytes) {
1065
+ if (bytes === 0) return '0 B';
1066
+ const k = 1024;
1067
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1068
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1069
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1070
+ }
1071
+
1072
+ // ========== EXPORT ==========
1073
+ document.getElementById('exportBtn').addEventListener('click', () => {
1074
+ const results = {
1075
+ file: selectedFile ? selectedFile.name : null,
1076
+ fileSize: selectedFile ? selectedFile.size : 0,
1077
+ strategy: document.getElementById('statStrategy').textContent,
1078
+ parts: parseInt(document.getElementById('statParts').textContent),
1079
+ vertices: parseInt(document.getElementById('statVertices').textContent.replace(/,/g, '')),
1080
+ faces: parseInt(document.getElementById('statFaces').textContent.replace(/,/g, '')),
1081
+ parseTime: document.getElementById('statParseTime').textContent,
1082
+ timestamp: new Date().toISOString()
1083
+ };
1084
+
1085
+ const json = JSON.stringify(results, null, 2);
1086
+ const blob = new Blob([json], { type: 'application/json' });
1087
+ const url = URL.createObjectURL(blob);
1088
+ const a = document.createElement('a');
1089
+ a.href = url;
1090
+ a.download = `step-import-results-${Date.now()}.json`;
1091
+ a.click();
1092
+ URL.revokeObjectURL(url);
1093
+
1094
+ log('Results exported', 'success');
1095
+ });
1096
+
1097
+ // ========== EVENT LISTENERS ==========
1098
+ document.getElementById('importBtn').addEventListener('click', importFile);
1099
+ document.getElementById('importBtn2').addEventListener('click', importFile);
1100
+
1101
+ document.getElementById('cancelBtn').addEventListener('click', () => {
1102
+ isImporting = false;
1103
+ updateStatus('idle', 'Idle');
1104
+ log('Import cancelled', 'warn');
1105
+ });
1106
+
1107
+ // ========== STARTUP ==========
1108
+ window.addEventListener('load', () => {
1109
+ initViewport();
1110
+ log('STEP Import Test Suite Ready', 'success');
1111
+ log('Select a STEP file to begin testing', 'info');
1112
+ });
1113
+ </script>
1114
+ </body>
1115
+ </html>