cyclecad 3.2.1 → 3.4.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 (65) hide show
  1. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  2. package/DOCKER-TESTING.md +463 -0
  3. package/FUSION360_MODULES.md +478 -0
  4. package/FUSION_MODULES_README.md +352 -0
  5. package/INTEGRATION_SNIPPETS.md +608 -0
  6. package/KILLER-FEATURES-DELIVERY.md +469 -0
  7. package/MODULES_SUMMARY.txt +337 -0
  8. package/QUICK_REFERENCE.txt +298 -0
  9. package/README-DOCKER-TESTING.txt +438 -0
  10. package/app/index.html +23 -10
  11. package/app/js/fusion-help.json +1808 -0
  12. package/app/js/help-module-v3.js +1096 -0
  13. package/app/js/killer-features-help.json +395 -0
  14. package/app/js/killer-features.js +1508 -0
  15. package/app/js/modules/fusion-assembly.js +842 -0
  16. package/app/js/modules/fusion-cam.js +785 -0
  17. package/app/js/modules/fusion-data.js +814 -0
  18. package/app/js/modules/fusion-drawing.js +844 -0
  19. package/app/js/modules/fusion-inspection.js +756 -0
  20. package/app/js/modules/fusion-render.js +774 -0
  21. package/app/js/modules/fusion-simulation.js +986 -0
  22. package/app/js/modules/fusion-sketch.js +1044 -0
  23. package/app/js/modules/fusion-solid.js +1095 -0
  24. package/app/js/modules/fusion-surface.js +949 -0
  25. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  26. package/app/tests/README.md +77 -0
  27. package/app/tests/TESTING-CHECKLIST.md +177 -0
  28. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  29. package/app/tests/brep-live-test.html +848 -0
  30. package/app/tests/docker-integration-test.html +811 -0
  31. package/app/tests/fusion-all-tests.html +670 -0
  32. package/app/tests/fusion-assembly-tests.html +461 -0
  33. package/app/tests/fusion-cam-tests.html +421 -0
  34. package/app/tests/fusion-simulation-tests.html +421 -0
  35. package/app/tests/fusion-sketch-tests.html +613 -0
  36. package/app/tests/fusion-solid-tests.html +529 -0
  37. package/app/tests/index.html +453 -0
  38. package/app/tests/killer-features-test.html +509 -0
  39. package/app/tests/run-tests.html +874 -0
  40. package/app/tests/step-import-live-test.html +1115 -0
  41. package/app/tests/test-agent-v3.html +93 -696
  42. package/architecture-dashboard.html +1970 -0
  43. package/docs/API-REFERENCE.md +1423 -0
  44. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  45. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  46. package/docs/DOCKER-QUICK-TEST.md +376 -0
  47. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  48. package/docs/FUSION-TUTORIAL.md +1203 -0
  49. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  50. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  51. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  52. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  53. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  54. package/docs/KILLER-FEATURES.md +562 -0
  55. package/docs/QUICK-REFERENCE.md +282 -0
  56. package/docs/README-v3-DOCS.md +274 -0
  57. package/docs/TUTORIAL-v3.md +1190 -0
  58. package/docs/architecture-dashboard.html +1970 -0
  59. package/docs/architecture-v3.html +1038 -0
  60. package/linkedin-post-v3.md +58 -0
  61. package/package.json +1 -1
  62. package/scripts/dev-setup.sh +338 -0
  63. package/scripts/docker-health-check.sh +159 -0
  64. package/scripts/integration-test.sh +311 -0
  65. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,848 @@
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>B-Rep Live Test — OpenCascade.js WASM + Three.js</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, sans-serif;
16
+ background: #2d2d30;
17
+ color: #cccccc;
18
+ height: 100vh;
19
+ overflow: hidden;
20
+ }
21
+
22
+ .container {
23
+ display: flex;
24
+ flex-direction: column;
25
+ height: 100vh;
26
+ }
27
+
28
+ .header {
29
+ background: #1e1e1e;
30
+ border-bottom: 1px solid #3e3e42;
31
+ padding: 16px 20px;
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 16px;
39
+ font-weight: 600;
40
+ letter-spacing: 0.5px;
41
+ }
42
+
43
+ .header-controls {
44
+ display: flex;
45
+ gap: 12px;
46
+ align-items: center;
47
+ }
48
+
49
+ .wasm-progress {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 8px;
53
+ font-size: 12px;
54
+ color: #999;
55
+ }
56
+
57
+ .progress-bar {
58
+ width: 120px;
59
+ height: 4px;
60
+ background: #3e3e42;
61
+ border-radius: 2px;
62
+ overflow: hidden;
63
+ }
64
+
65
+ .progress-fill {
66
+ height: 100%;
67
+ background: #007acc;
68
+ width: 0%;
69
+ transition: width 0.1s linear;
70
+ }
71
+
72
+ button {
73
+ background: #007acc;
74
+ color: white;
75
+ border: none;
76
+ padding: 8px 16px;
77
+ border-radius: 4px;
78
+ font-size: 13px;
79
+ font-weight: 500;
80
+ cursor: pointer;
81
+ transition: background 0.2s;
82
+ }
83
+
84
+ button:hover:not(:disabled) {
85
+ background: #1084d7;
86
+ }
87
+
88
+ button:disabled {
89
+ background: #3e3e42;
90
+ color: #858585;
91
+ cursor: not-allowed;
92
+ }
93
+
94
+ .main {
95
+ display: flex;
96
+ flex: 1;
97
+ overflow: hidden;
98
+ gap: 0;
99
+ }
100
+
101
+ .sidebar {
102
+ width: 300px;
103
+ background: #252526;
104
+ border-right: 1px solid #3e3e42;
105
+ display: flex;
106
+ flex-direction: column;
107
+ overflow: hidden;
108
+ }
109
+
110
+ .sidebar-header {
111
+ padding: 12px 16px;
112
+ border-bottom: 1px solid #3e3e42;
113
+ font-size: 12px;
114
+ font-weight: 600;
115
+ text-transform: uppercase;
116
+ color: #858585;
117
+ letter-spacing: 0.5px;
118
+ }
119
+
120
+ .test-list {
121
+ flex: 1;
122
+ overflow-y: auto;
123
+ padding: 8px;
124
+ }
125
+
126
+ .test-item {
127
+ padding: 10px 12px;
128
+ margin-bottom: 4px;
129
+ background: #2d2d30;
130
+ border: 1px solid #3e3e42;
131
+ border-radius: 4px;
132
+ font-size: 13px;
133
+ cursor: pointer;
134
+ transition: all 0.2s;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 8px;
138
+ }
139
+
140
+ .test-item:hover {
141
+ background: #323236;
142
+ border-color: #007acc;
143
+ }
144
+
145
+ .test-item.running {
146
+ background: #1e1e1e;
147
+ border-color: #f4d03f;
148
+ }
149
+
150
+ .test-item.passed {
151
+ border-color: #4ec9b0;
152
+ background: #1a3a3a;
153
+ }
154
+
155
+ .test-item.failed {
156
+ border-color: #f48771;
157
+ background: #3a1a1a;
158
+ }
159
+
160
+ .test-status {
161
+ width: 16px;
162
+ height: 16px;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ font-size: 11px;
167
+ font-weight: bold;
168
+ border-radius: 2px;
169
+ flex-shrink: 0;
170
+ }
171
+
172
+ .test-status.pending {
173
+ background: #3e3e42;
174
+ color: #999;
175
+ }
176
+
177
+ .test-status.running {
178
+ background: #f4d03f;
179
+ color: #1e1e1e;
180
+ animation: pulse 1s infinite;
181
+ }
182
+
183
+ .test-status.passed {
184
+ background: #4ec9b0;
185
+ color: #1e1e1e;
186
+ }
187
+
188
+ .test-status.failed {
189
+ background: #f48771;
190
+ color: #1e1e1e;
191
+ }
192
+
193
+ @keyframes pulse {
194
+ 0%, 100% { opacity: 1; }
195
+ 50% { opacity: 0.6; }
196
+ }
197
+
198
+ .test-label {
199
+ flex: 1;
200
+ display: flex;
201
+ flex-direction: column;
202
+ }
203
+
204
+ .test-name {
205
+ font-weight: 500;
206
+ }
207
+
208
+ .test-time {
209
+ font-size: 11px;
210
+ color: #858585;
211
+ margin-top: 2px;
212
+ }
213
+
214
+ .viewport {
215
+ flex: 1;
216
+ background: #1e1e1e;
217
+ position: relative;
218
+ overflow: hidden;
219
+ }
220
+
221
+ canvas {
222
+ display: block;
223
+ width: 100%;
224
+ height: 100%;
225
+ }
226
+
227
+ .stats-overlay {
228
+ position: absolute;
229
+ top: 16px;
230
+ right: 16px;
231
+ background: rgba(29, 29, 29, 0.9);
232
+ border: 1px solid #3e3e42;
233
+ border-radius: 4px;
234
+ padding: 12px;
235
+ font-size: 12px;
236
+ font-family: 'Courier New', monospace;
237
+ color: #4ec9b0;
238
+ pointer-events: none;
239
+ z-index: 10;
240
+ max-width: 250px;
241
+ }
242
+
243
+ .stats-row {
244
+ display: flex;
245
+ justify-content: space-between;
246
+ gap: 16px;
247
+ margin-bottom: 6px;
248
+ white-space: nowrap;
249
+ }
250
+
251
+ .stats-row:last-child {
252
+ margin-bottom: 0;
253
+ }
254
+
255
+ .stats-label {
256
+ color: #858585;
257
+ }
258
+
259
+ .footer {
260
+ background: #1e1e1e;
261
+ border-top: 1px solid #3e3e42;
262
+ padding: 12px 16px;
263
+ font-size: 12px;
264
+ max-height: 120px;
265
+ overflow-y: auto;
266
+ color: #999;
267
+ }
268
+
269
+ .log-entry {
270
+ margin-bottom: 4px;
271
+ display: flex;
272
+ gap: 8px;
273
+ }
274
+
275
+ .log-time {
276
+ color: #858585;
277
+ flex-shrink: 0;
278
+ }
279
+
280
+ .log-text {
281
+ flex: 1;
282
+ }
283
+
284
+ .log-error {
285
+ color: #f48771;
286
+ }
287
+
288
+ .log-success {
289
+ color: #4ec9b0;
290
+ }
291
+
292
+ .log-warning {
293
+ color: #f4d03f;
294
+ }
295
+
296
+ .error-banner {
297
+ background: #3a1a1a;
298
+ border: 1px solid #f48771;
299
+ border-radius: 4px;
300
+ padding: 12px;
301
+ margin: 12px;
302
+ color: #f48771;
303
+ font-size: 13px;
304
+ }
305
+
306
+ .error-banner strong {
307
+ display: block;
308
+ margin-bottom: 4px;
309
+ }
310
+ </style>
311
+ </head>
312
+ <body>
313
+ <div class="container">
314
+ <div class="header">
315
+ <h1>B-Rep Live Test — OpenCascade.js WASM + Three.js</h1>
316
+ <div class="header-controls">
317
+ <div class="wasm-progress">
318
+ <span>WASM:</span>
319
+ <div class="progress-bar">
320
+ <div class="progress-fill" id="wasmProgress"></div>
321
+ </div>
322
+ <span id="wasmStatus">Loading...</span>
323
+ </div>
324
+ <button id="runAllBtn" disabled>Run All Tests</button>
325
+ </div>
326
+ </div>
327
+
328
+ <div class="main">
329
+ <div class="sidebar">
330
+ <div class="sidebar-header">Tests (14 total)</div>
331
+ <div class="test-list" id="testList"></div>
332
+ </div>
333
+
334
+ <div class="viewport">
335
+ <canvas id="canvas"></canvas>
336
+ <div class="stats-overlay" id="statsOverlay">
337
+ <div class="stats-row">
338
+ <span class="stats-label">FPS:</span>
339
+ <span id="statsFPS">0</span>
340
+ </div>
341
+ <div class="stats-row">
342
+ <span class="stats-label">Triangles:</span>
343
+ <span id="statsTriangles">0</span>
344
+ </div>
345
+ <div class="stats-row">
346
+ <span class="stats-label">Vertices:</span>
347
+ <span id="statsVertices">0</span>
348
+ </div>
349
+ <div class="stats-row">
350
+ <span class="stats-label">Operation:</span>
351
+ <span id="statsOp">—</span>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <div class="footer">
358
+ <div id="logContainer"></div>
359
+ </div>
360
+ </div>
361
+
362
+ <script type="module">
363
+ const CDN_BASE = 'https://cdn.jsdelivr.net/npm/opencascade.js@2.0.0-beta.b5ff984/dist/';
364
+ const THREE_CDN = 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
365
+
366
+ // Import Three.js
367
+ import * as THREE from THREE_CDN;
368
+
369
+ // Globals
370
+ let oc = null;
371
+ let scene, camera, renderer, controls;
372
+ let currentMesh = null;
373
+ let frameCount = 0;
374
+ let fps = 0;
375
+ let lastFpsTime = Date.now();
376
+
377
+ const tests = [
378
+ { id: 'box', name: 'Box (100x50x30)', category: 'Primitives' },
379
+ { id: 'cylinder', name: 'Cylinder (r=25, h=60)', category: 'Primitives' },
380
+ { id: 'sphere', name: 'Sphere (r=30)', category: 'Primitives' },
381
+ { id: 'cone', name: 'Cone (r1=20, r2=5, h=50)', category: 'Primitives' },
382
+ { id: 'torus', name: 'Torus (major=30, minor=8)', category: 'Primitives' },
383
+ { id: 'fillet', name: 'Fillet (box + 5mm edges)', category: 'Operations' },
384
+ { id: 'chamfer', name: 'Chamfer (box + 3mm)', category: 'Operations' },
385
+ { id: 'union', name: 'Boolean Union (box + cyl)', category: 'Operations' },
386
+ { id: 'cut', name: 'Boolean Cut (box - cyl)', category: 'Operations' },
387
+ { id: 'intersect', name: 'Boolean Intersect (box ∩ sphere)', category: 'Operations' },
388
+ { id: 'extrude', name: 'Extrude (face → 3D)', category: 'Operations' },
389
+ { id: 'mass', name: 'Mass Properties (volume/area)', category: 'Analysis' },
390
+ { id: 'edges', name: 'Edge Count (topological)', category: 'Analysis' },
391
+ { id: 'faces', name: 'Face Count (topological)', category: 'Analysis' },
392
+ ];
393
+
394
+ const testResults = {};
395
+
396
+ function log(message, type = 'info') {
397
+ const container = document.getElementById('logContainer');
398
+ const entry = document.createElement('div');
399
+ entry.className = `log-entry`;
400
+
401
+ const time = new Date().toLocaleTimeString();
402
+ const timeEl = document.createElement('span');
403
+ timeEl.className = 'log-time';
404
+ timeEl.textContent = `[${time}]`;
405
+
406
+ const textEl = document.createElement('span');
407
+ textEl.className = `log-text ${type === 'error' ? 'log-error' : type === 'success' ? 'log-success' : type === 'warning' ? 'log-warning' : ''}`;
408
+ textEl.textContent = message;
409
+
410
+ entry.appendChild(timeEl);
411
+ entry.appendChild(textEl);
412
+ container.appendChild(entry);
413
+ container.scrollTop = container.scrollHeight;
414
+ }
415
+
416
+ async function loadOpenCascade() {
417
+ try {
418
+ log('Downloading OpenCascade.js WASM (~50MB)...', 'warning');
419
+
420
+ // Fetch JS file
421
+ const jsUrl = `${CDN_BASE}opencascade.full.js`;
422
+ const jsResp = await fetch(jsUrl);
423
+ if (!jsResp.ok) throw new Error(`Failed to fetch JS: ${jsResp.status}`);
424
+
425
+ // Fetch WASM file with progress
426
+ const wasmUrl = `${CDN_BASE}opencascade.full.wasm`;
427
+ const wasmResp = await fetch(wasmUrl);
428
+ if (!wasmResp.ok) throw new Error(`Failed to fetch WASM: ${wasmResp.status}`);
429
+
430
+ const total = parseInt(wasmResp.headers.get('content-length') || '0', 10);
431
+ let loaded = 0;
432
+ const reader = wasmResp.body.getReader();
433
+ const chunks = [];
434
+
435
+ while (true) {
436
+ const { done, value } = await reader.read();
437
+ if (done) break;
438
+ chunks.push(value);
439
+ loaded += value.length;
440
+ const progress = total ? (loaded / total) * 100 : 0;
441
+ document.getElementById('wasmProgress').style.width = progress + '%';
442
+ document.getElementById('wasmStatus').textContent = `${Math.round(progress)}%`;
443
+ }
444
+
445
+ const wasmBuffer = new Uint8Array(loaded);
446
+ let offset = 0;
447
+ for (const chunk of chunks) {
448
+ wasmBuffer.set(chunk, offset);
449
+ offset += chunk.length;
450
+ }
451
+
452
+ log(`WASM loaded: ${(wasmBuffer.length / 1024 / 1024).toFixed(1)}MB`, 'success');
453
+
454
+ // Create blob and evaluate JS
455
+ const jsBlob = await jsResp.blob();
456
+ const jsText = await jsBlob.text();
457
+
458
+ // Create a temporary Module object to capture the factory
459
+ window.Module = undefined;
460
+ const script = document.createElement('script');
461
+ script.textContent = jsText;
462
+ document.head.appendChild(script);
463
+
464
+ // Wait for Module to be defined and initialized
465
+ while (!window.Module || !window.Module.onRuntimeInitialized) {
466
+ await new Promise(r => setTimeout(r, 100));
467
+ }
468
+
469
+ // Initialize with WASM binary
470
+ oc = await new window.Module({
471
+ wasmBinary: wasmBuffer,
472
+ locateFile: (filename) => `${CDN_BASE}${filename}`
473
+ });
474
+
475
+ log('OpenCascade.js initialized successfully', 'success');
476
+ document.getElementById('wasmStatus').textContent = 'Ready';
477
+ document.getElementById('runAllBtn').disabled = false;
478
+ return true;
479
+ } catch (err) {
480
+ log(`Failed to load OpenCascade.js: ${err.message}`, 'error');
481
+ return false;
482
+ }
483
+ }
484
+
485
+ function initThreeJS() {
486
+ const canvas = document.getElementById('canvas');
487
+
488
+ scene = new THREE.Scene();
489
+ scene.background = new THREE.Color(0x1e1e1e);
490
+ scene.fog = new THREE.Fog(0x1e1e1e, 500, 1000);
491
+
492
+ camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 5000);
493
+ camera.position.set(150, 120, 150);
494
+
495
+ renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
496
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
497
+ renderer.shadowMap.enabled = true;
498
+
499
+ // Lights
500
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
501
+ scene.add(ambientLight);
502
+
503
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
504
+ directionalLight.position.set(200, 200, 200);
505
+ directionalLight.castShadow = true;
506
+ directionalLight.shadow.mapSize.width = 2048;
507
+ directionalLight.shadow.mapSize.height = 2048;
508
+ directionalLight.shadow.camera.left = -500;
509
+ directionalLight.shadow.camera.right = 500;
510
+ directionalLight.shadow.camera.top = 500;
511
+ directionalLight.shadow.camera.bottom = -500;
512
+ scene.add(directionalLight);
513
+
514
+ // Grid
515
+ const gridHelper = new THREE.GridHelper(400, 20, 0x444444, 0x222222);
516
+ scene.add(gridHelper);
517
+
518
+ // Controls
519
+ const OrbitControls = await import(`${THREE_CDN.replace('three.module.js', 'examples/jsm/controls/OrbitControls.js')}`);
520
+ controls = new OrbitControls.OrbitControls(camera, renderer.domElement);
521
+ controls.enableDamping = true;
522
+ controls.dampingFactor = 0.05;
523
+ controls.autoRotate = false;
524
+
525
+ // Animation loop
526
+ function animate() {
527
+ requestAnimationFrame(animate);
528
+
529
+ frameCount++;
530
+ const now = Date.now();
531
+ if (now - lastFpsTime >= 1000) {
532
+ fps = frameCount;
533
+ frameCount = 0;
534
+ lastFpsTime = now;
535
+ }
536
+
537
+ document.getElementById('statsFPS').textContent = fps;
538
+
539
+ controls.update();
540
+ renderer.render(scene, camera);
541
+ }
542
+
543
+ animate();
544
+
545
+ log('Three.js scene initialized', 'success');
546
+ }
547
+
548
+ function clearViewport() {
549
+ if (currentMesh) {
550
+ scene.remove(currentMesh);
551
+ if (currentMesh.geometry) currentMesh.geometry.dispose();
552
+ if (currentMesh.material) currentMesh.material.dispose();
553
+ currentMesh = null;
554
+ }
555
+ }
556
+
557
+ function tessellateShape(shape, linearDeflection = 0.1) {
558
+ try {
559
+ // Mesh the shape
560
+ new oc.BRepMesh_IncrementalMesh(shape, linearDeflection);
561
+
562
+ const vertices = [];
563
+ const indices = [];
564
+ let vertexOffset = 0;
565
+
566
+ // Iterate faces
567
+ const faceExplorer = new oc.TopExp_Explorer(shape, oc.TopAbs_ShapeEnum.TopAbs_FACE);
568
+
569
+ while (faceExplorer.More()) {
570
+ const face = oc.TopoDS.Face(faceExplorer.Current());
571
+ const triangulation = oc.BRep_Tool.Triangulation(face);
572
+
573
+ if (triangulation) {
574
+ const nodes = triangulation.Nodes();
575
+ const triangles = triangulation.Triangles();
576
+
577
+ // Add vertices
578
+ for (let i = 1; i <= nodes.Length(); i++) {
579
+ const node = nodes.Value(i);
580
+ vertices.push(node.X(), node.Y(), node.Z());
581
+ }
582
+
583
+ // Add indices
584
+ for (let i = 1; i <= triangles.Length(); i++) {
585
+ const tri = triangles.Value(i);
586
+ indices.push(
587
+ tri.Value(1) - 1 + vertexOffset,
588
+ tri.Value(2) - 1 + vertexOffset,
589
+ tri.Value(3) - 1 + vertexOffset
590
+ );
591
+ }
592
+
593
+ vertexOffset += nodes.Length();
594
+ }
595
+
596
+ faceExplorer.Next();
597
+ }
598
+
599
+ return { vertices, indices };
600
+ } catch (err) {
601
+ log(`Tessellation error: ${err.message}`, 'error');
602
+ return null;
603
+ }
604
+ }
605
+
606
+ function renderGeometry(vertices, indices) {
607
+ clearViewport();
608
+
609
+ const geometry = new THREE.BufferGeometry();
610
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
611
+ geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
612
+ geometry.computeVertexNormals();
613
+
614
+ const material = new THREE.MeshStandardMaterial({
615
+ color: 0x007acc,
616
+ metalness: 0.3,
617
+ roughness: 0.4,
618
+ side: THREE.DoubleSide
619
+ });
620
+
621
+ currentMesh = new THREE.Mesh(geometry, material);
622
+ currentMesh.castShadow = true;
623
+ currentMesh.receiveShadow = true;
624
+ scene.add(currentMesh);
625
+
626
+ // Fit camera
627
+ const bbox = new THREE.Box3().setFromObject(currentMesh);
628
+ const size = bbox.getSize(new THREE.Vector3());
629
+ const maxDim = Math.max(size.x, size.y, size.z);
630
+ const fov = camera.fov * (Math.PI / 180);
631
+ let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
632
+ cameraZ *= 1.5;
633
+ camera.position.z = cameraZ;
634
+ camera.lookAt(bbox.getCenter(new THREE.Vector3()));
635
+ controls.target.copy(bbox.getCenter(new THREE.Vector3()));
636
+ controls.update();
637
+
638
+ const triangleCount = indices.length / 3;
639
+ const vertexCount = vertices.length / 3;
640
+ document.getElementById('statsTriangles').textContent = triangleCount.toLocaleString();
641
+ document.getElementById('statsVertices').textContent = vertexCount.toLocaleString();
642
+
643
+ return { triangleCount, vertexCount };
644
+ }
645
+
646
+ async function runTest(testId) {
647
+ const test = tests.find(t => t.id === testId);
648
+ if (!test) return;
649
+
650
+ const startTime = performance.now();
651
+ updateTestStatus(testId, 'running');
652
+ document.getElementById('statsOp').textContent = test.name;
653
+
654
+ try {
655
+ let shape = null;
656
+
657
+ if (testId === 'box') {
658
+ shape = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
659
+ } else if (testId === 'cylinder') {
660
+ shape = new oc.BRepPrimAPI_MakeCylinder_2(25, 60).Shape();
661
+ } else if (testId === 'sphere') {
662
+ shape = new oc.BRepPrimAPI_MakeSphere_1(30).Shape();
663
+ } else if (testId === 'cone') {
664
+ shape = new oc.BRepPrimAPI_MakeCone_3(20, 5, 50).Shape();
665
+ } else if (testId === 'torus') {
666
+ shape = new oc.BRepPrimAPI_MakeTorus_3(30, 8).Shape();
667
+ } else if (testId === 'fillet') {
668
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
669
+ const makeFillet = new oc.BRepFilletAPI_MakeFillet(box);
670
+ const edgeExplorer = new oc.TopExp_Explorer(box, oc.TopAbs_ShapeEnum.TopAbs_EDGE);
671
+
672
+ while (edgeExplorer.More()) {
673
+ const edge = oc.TopoDS.Edge(edgeExplorer.Current());
674
+ makeFillet.Add(5, edge);
675
+ edgeExplorer.Next();
676
+ }
677
+
678
+ shape = makeFillet.Shape();
679
+ } else if (testId === 'chamfer') {
680
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
681
+ const makeChamfer = new oc.BRepFilletAPI_MakeChamfer(box);
682
+ const edgeExplorer = new oc.TopExp_Explorer(box, oc.TopAbs_ShapeEnum.TopAbs_EDGE);
683
+
684
+ while (edgeExplorer.More()) {
685
+ const edge = oc.TopoDS.Edge(edgeExplorer.Current());
686
+ makeChamfer.Add(3, edge);
687
+ edgeExplorer.Next();
688
+ }
689
+
690
+ shape = makeChamfer.Shape();
691
+ } else if (testId === 'union') {
692
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
693
+ const cylinder = new oc.BRepPrimAPI_MakeCylinder_2(20, 60).Shape();
694
+ shape = new oc.BRepAlgoAPI_Fuse(box, cylinder).Shape();
695
+ } else if (testId === 'cut') {
696
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
697
+ const cylinder = new oc.BRepPrimAPI_MakeCylinder_2(20, 60).Shape();
698
+ shape = new oc.BRepAlgoAPI_Cut(box, cylinder).Shape();
699
+ } else if (testId === 'intersect') {
700
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
701
+ const sphere = new oc.BRepPrimAPI_MakeSphere_1(30).Shape();
702
+ shape = new oc.BRepAlgoAPI_Common(box, sphere).Shape();
703
+ } else if (testId === 'extrude') {
704
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
705
+ const faceExplorer = new oc.TopExp_Explorer(box, oc.TopAbs_ShapeEnum.TopAbs_FACE);
706
+ const face = oc.TopoDS.Face(faceExplorer.Current());
707
+ const direction = new oc.gp_Dir_3(0, 0, 1);
708
+ shape = new oc.BRepPrimAPI_MakePrism(face, new oc.gp_Vec_1(direction, 50)).Shape();
709
+ } else if (testId === 'mass') {
710
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
711
+ const props = new oc.GProp_GProps();
712
+ oc.BRepGProp.VolumeProperties(box, props);
713
+ const volume = props.Mass();
714
+
715
+ oc.BRepGProp.SurfaceProperties(box, props);
716
+ const area = props.Mass();
717
+
718
+ log(`Box volume: ${volume.toFixed(2)} mm³, area: ${area.toFixed(2)} mm²`, 'success');
719
+ document.getElementById('statsOp').textContent = `Vol: ${volume.toFixed(0)}, Area: ${area.toFixed(0)}`;
720
+ updateTestStatus(testId, 'passed', performance.now() - startTime);
721
+ return;
722
+ } else if (testId === 'edges') {
723
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
724
+ const edgeExplorer = new oc.TopExp_Explorer(box, oc.TopAbs_ShapeEnum.TopAbs_EDGE);
725
+ let edgeCount = 0;
726
+
727
+ while (edgeExplorer.More()) {
728
+ edgeCount++;
729
+ edgeExplorer.Next();
730
+ }
731
+
732
+ log(`Box has ${edgeCount} edges`, 'success');
733
+ document.getElementById('statsOp').textContent = `Edges: ${edgeCount}`;
734
+ updateTestStatus(testId, 'passed', performance.now() - startTime);
735
+ return;
736
+ } else if (testId === 'faces') {
737
+ const box = new oc.BRepPrimAPI_MakeBox_3(100, 50, 30).Shape();
738
+ const faceExplorer = new oc.TopExp_Explorer(box, oc.TopAbs_ShapeEnum.TopAbs_FACE);
739
+ let faceCount = 0;
740
+
741
+ while (faceExplorer.More()) {
742
+ faceCount++;
743
+ faceExplorer.Next();
744
+ }
745
+
746
+ log(`Box has ${faceCount} faces`, 'success');
747
+ document.getElementById('statsOp').textContent = `Faces: ${faceCount}`;
748
+ updateTestStatus(testId, 'passed', performance.now() - startTime);
749
+ return;
750
+ }
751
+
752
+ if (!shape) throw new Error('Failed to create shape');
753
+
754
+ const tessData = tessellateShape(shape);
755
+ if (!tessData) throw new Error('Tessellation failed');
756
+
757
+ const { triangleCount, vertexCount } = renderGeometry(tessData.vertices, tessData.indices);
758
+
759
+ const elapsed = performance.now() - startTime;
760
+ log(`${test.name}: ${elapsed.toFixed(1)}ms, ${triangleCount} triangles`, 'success');
761
+ updateTestStatus(testId, 'passed', elapsed);
762
+ } catch (err) {
763
+ log(`${test.name}: ${err.message}`, 'error');
764
+ updateTestStatus(testId, 'failed', performance.now() - startTime);
765
+ }
766
+ }
767
+
768
+ function updateTestStatus(testId, status, elapsed = 0) {
769
+ const item = document.querySelector(`[data-test-id="${testId}"]`);
770
+ if (!item) return;
771
+
772
+ item.classList.remove('running', 'passed', 'failed');
773
+ item.classList.add(status);
774
+
775
+ const statusEl = item.querySelector('.test-status');
776
+ statusEl.classList.remove('pending', 'running', 'passed', 'failed');
777
+ statusEl.classList.add(status);
778
+
779
+ if (status === 'running') {
780
+ statusEl.textContent = '⟳';
781
+ } else if (status === 'passed') {
782
+ statusEl.textContent = '✓';
783
+ const timeEl = item.querySelector('.test-time');
784
+ timeEl.textContent = `${elapsed.toFixed(1)}ms`;
785
+ } else if (status === 'failed') {
786
+ statusEl.textContent = '✕';
787
+ const timeEl = item.querySelector('.test-time');
788
+ timeEl.textContent = `${elapsed.toFixed(1)}ms`;
789
+ }
790
+
791
+ testResults[testId] = { status, elapsed };
792
+ }
793
+
794
+ function renderTestList() {
795
+ const container = document.getElementById('testList');
796
+ container.innerHTML = '';
797
+
798
+ let currentCategory = '';
799
+ for (const test of tests) {
800
+ if (test.category !== currentCategory) {
801
+ currentCategory = test.category;
802
+ const categoryDiv = document.createElement('div');
803
+ categoryDiv.style.cssText = 'padding: 8px 12px; margin-top: 12px; font-size: 11px; text-transform: uppercase; color: #858585; border-bottom: 1px solid #3e3e42;';
804
+ categoryDiv.textContent = test.category;
805
+ container.appendChild(categoryDiv);
806
+ }
807
+
808
+ const item = document.createElement('div');
809
+ item.className = 'test-item';
810
+ item.setAttribute('data-test-id', test.id);
811
+
812
+ item.innerHTML = `
813
+ <div class="test-status pending">—</div>
814
+ <div class="test-label">
815
+ <div class="test-name">${test.name}</div>
816
+ <div class="test-time"></div>
817
+ </div>
818
+ `;
819
+
820
+ item.addEventListener('click', () => runTest(test.id));
821
+ container.appendChild(item);
822
+ }
823
+ }
824
+
825
+ async function runAllTests() {
826
+ for (const test of tests) {
827
+ await runTest(test.id);
828
+ await new Promise(r => setTimeout(r, 200));
829
+ }
830
+
831
+ const passed = Object.values(testResults).filter(r => r.status === 'passed').length;
832
+ const failed = Object.values(testResults).filter(r => r.status === 'failed').length;
833
+ log(`All tests completed: ${passed} passed, ${failed} failed`, passed > failed ? 'success' : 'warning');
834
+ }
835
+
836
+ // Init
837
+ log('Starting B-Rep Live Test...', 'info');
838
+ renderTestList();
839
+
840
+ const success = await loadOpenCascade();
841
+ if (success) {
842
+ await initThreeJS();
843
+ document.getElementById('runAllBtn').addEventListener('click', runAllTests);
844
+ log('Ready to run tests. Click "Run All" or select individual tests.', 'success');
845
+ }
846
+ </script>
847
+ </body>
848
+ </html>