cyclecad 3.0.0 → 3.1.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/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  7. package/DOCKER-FILES-REFERENCE.md +440 -0
  8. package/DOCKER-INFRASTRUCTURE.md +475 -0
  9. package/DOCKER-README.md +435 -0
  10. package/Dockerfile +33 -55
  11. package/PWA-FILES-CREATED.txt +350 -0
  12. package/QUICK-START-TESTING.md +126 -0
  13. package/STEP-IMPORT-QUICKSTART.md +347 -0
  14. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  15. package/app/css/mobile.css +1074 -0
  16. package/app/icons/generate-icons.js +203 -0
  17. package/app/js/billing-ui.js +990 -0
  18. package/app/js/brep-kernel.js +933 -981
  19. package/app/js/collab-client.js +750 -0
  20. package/app/js/mobile-nav.js +623 -0
  21. package/app/js/mobile-toolbar.js +476 -0
  22. package/app/js/modules/billing-module.js +724 -0
  23. package/app/js/modules/step-module-enhanced.js +938 -0
  24. package/app/js/offline-manager.js +705 -0
  25. package/app/js/responsive-init.js +360 -0
  26. package/app/js/touch-handler.js +429 -0
  27. package/app/manifest.json +211 -0
  28. package/app/offline.html +508 -0
  29. package/app/sw.js +571 -0
  30. package/app/tests/billing-tests.html +779 -0
  31. package/app/tests/brep-tests.html +980 -0
  32. package/app/tests/collab-tests.html +743 -0
  33. package/app/tests/mobile-tests.html +1299 -0
  34. package/app/tests/pwa-tests.html +1134 -0
  35. package/app/tests/step-tests.html +1042 -0
  36. package/app/tests/test-agent-v3.html +719 -0
  37. package/docker-compose.yml +225 -0
  38. package/docs/BILLING-HELP.json +260 -0
  39. package/docs/BILLING-README.md +639 -0
  40. package/docs/BILLING-TUTORIAL.md +736 -0
  41. package/docs/BREP-HELP.json +326 -0
  42. package/docs/BREP-TUTORIAL.md +802 -0
  43. package/docs/COLLABORATION-HELP.json +228 -0
  44. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  45. package/docs/DOCKER-HELP.json +224 -0
  46. package/docs/DOCKER-TUTORIAL.md +974 -0
  47. package/docs/MOBILE-HELP.json +243 -0
  48. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  49. package/docs/MOBILE-TUTORIAL.md +747 -0
  50. package/docs/PWA-HELP.json +228 -0
  51. package/docs/PWA-README.md +662 -0
  52. package/docs/PWA-TUTORIAL.md +757 -0
  53. package/docs/STEP-HELP.json +481 -0
  54. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  55. package/docs/TESTING-GUIDE.md +528 -0
  56. package/docs/TESTING-HELP.json +182 -0
  57. package/fusion-vs-cyclecad.html +1771 -0
  58. package/nginx.conf +237 -0
  59. package/package.json +1 -1
  60. package/server/Dockerfile.converter +51 -0
  61. package/server/Dockerfile.signaling +28 -0
  62. package/server/billing-server.js +487 -0
  63. package/server/converter-enhanced.py +528 -0
  64. package/server/requirements-converter.txt +29 -0
  65. package/server/signaling-server.js +801 -0
  66. package/tests/docker-tests.sh +389 -0
@@ -0,0 +1,980 @@
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 Kernel Test Suite</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: #1e1e1e;
17
+ color: #e0e0e0;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1600px;
23
+ margin: 0 auto;
24
+ display: grid;
25
+ grid-template-columns: 300px 1fr 400px;
26
+ gap: 20px;
27
+ height: calc(100vh - 40px);
28
+ }
29
+
30
+ /* Left Panel: Test Categories */
31
+ .left-panel {
32
+ background: #252525;
33
+ border-radius: 8px;
34
+ padding: 15px;
35
+ overflow-y: auto;
36
+ border: 1px solid #404040;
37
+ }
38
+
39
+ .category {
40
+ margin-bottom: 10px;
41
+ }
42
+
43
+ .category-title {
44
+ font-size: 12px;
45
+ font-weight: bold;
46
+ color: #6fa3d0;
47
+ text-transform: uppercase;
48
+ padding: 8px 5px;
49
+ cursor: pointer;
50
+ user-select: none;
51
+ border-radius: 4px;
52
+ transition: background 0.2s;
53
+ }
54
+
55
+ .category-title:hover {
56
+ background: #333333;
57
+ }
58
+
59
+ .category-title.active {
60
+ background: #6fa3d0;
61
+ color: #1e1e1e;
62
+ }
63
+
64
+ .test-list {
65
+ display: none;
66
+ padding-left: 10px;
67
+ }
68
+
69
+ .test-list.active {
70
+ display: block;
71
+ }
72
+
73
+ .test-item {
74
+ font-size: 11px;
75
+ padding: 6px 8px;
76
+ margin: 3px 0;
77
+ background: #2d2d2d;
78
+ border-radius: 3px;
79
+ cursor: pointer;
80
+ transition: all 0.2s;
81
+ white-space: nowrap;
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ }
85
+
86
+ .test-item:hover {
87
+ background: #363636;
88
+ }
89
+
90
+ .test-item.pass {
91
+ border-left: 3px solid #4caf50;
92
+ }
93
+
94
+ .test-item.fail {
95
+ border-left: 3px solid #f44336;
96
+ }
97
+
98
+ .test-item.skip {
99
+ border-left: 3px solid #ff9800;
100
+ }
101
+
102
+ /* Center Panel: 3D Viewport */
103
+ .center-panel {
104
+ background: #0a0a0a;
105
+ border-radius: 8px;
106
+ overflow: hidden;
107
+ border: 1px solid #404040;
108
+ position: relative;
109
+ }
110
+
111
+ #viewport {
112
+ width: 100%;
113
+ height: 100%;
114
+ display: block;
115
+ }
116
+
117
+ .viewport-info {
118
+ position: absolute;
119
+ top: 10px;
120
+ left: 10px;
121
+ background: rgba(0, 0, 0, 0.8);
122
+ padding: 10px 15px;
123
+ border-radius: 4px;
124
+ font-size: 11px;
125
+ color: #4caf50;
126
+ font-family: 'Courier New', monospace;
127
+ max-width: 200px;
128
+ }
129
+
130
+ /* Right Panel: Test Log */
131
+ .right-panel {
132
+ background: #252525;
133
+ border-radius: 8px;
134
+ padding: 15px;
135
+ overflow-y: auto;
136
+ border: 1px solid #404040;
137
+ display: flex;
138
+ flex-direction: column;
139
+ }
140
+
141
+ .controls {
142
+ display: flex;
143
+ gap: 8px;
144
+ margin-bottom: 15px;
145
+ flex-wrap: wrap;
146
+ }
147
+
148
+ button {
149
+ padding: 8px 12px;
150
+ background: #6fa3d0;
151
+ color: #1e1e1e;
152
+ border: none;
153
+ border-radius: 4px;
154
+ font-size: 11px;
155
+ font-weight: bold;
156
+ cursor: pointer;
157
+ transition: all 0.2s;
158
+ }
159
+
160
+ button:hover {
161
+ background: #81b3e0;
162
+ transform: translateY(-1px);
163
+ }
164
+
165
+ button:active {
166
+ transform: translateY(0);
167
+ }
168
+
169
+ button.secondary {
170
+ background: #404040;
171
+ color: #e0e0e0;
172
+ }
173
+
174
+ button.secondary:hover {
175
+ background: #505050;
176
+ }
177
+
178
+ .progress-bar {
179
+ width: 100%;
180
+ height: 20px;
181
+ background: #1e1e1e;
182
+ border-radius: 4px;
183
+ overflow: hidden;
184
+ margin-bottom: 10px;
185
+ }
186
+
187
+ .progress-fill {
188
+ height: 100%;
189
+ background: linear-gradient(90deg, #4caf50, #81c784);
190
+ width: 0%;
191
+ transition: width 0.3s;
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: center;
195
+ font-size: 10px;
196
+ font-weight: bold;
197
+ color: #1e1e1e;
198
+ }
199
+
200
+ .stats {
201
+ display: grid;
202
+ grid-template-columns: 1fr 1fr;
203
+ gap: 8px;
204
+ margin-bottom: 15px;
205
+ font-size: 11px;
206
+ }
207
+
208
+ .stat-card {
209
+ background: #1e1e1e;
210
+ padding: 8px;
211
+ border-radius: 4px;
212
+ border-left: 3px solid #6fa3d0;
213
+ text-align: center;
214
+ }
215
+
216
+ .stat-value {
217
+ font-size: 14px;
218
+ font-weight: bold;
219
+ color: #4caf50;
220
+ }
221
+
222
+ .stat-label {
223
+ font-size: 9px;
224
+ color: #999;
225
+ margin-top: 2px;
226
+ }
227
+
228
+ .test-log {
229
+ flex: 1;
230
+ overflow-y: auto;
231
+ font-size: 10px;
232
+ font-family: 'Courier New', monospace;
233
+ background: #1e1e1e;
234
+ padding: 10px;
235
+ border-radius: 4px;
236
+ border: 1px solid #333333;
237
+ }
238
+
239
+ .log-entry {
240
+ padding: 4px 0;
241
+ line-height: 1.4;
242
+ border-bottom: 1px solid #2d2d2d;
243
+ display: none;
244
+ }
245
+
246
+ .log-entry.show {
247
+ display: block;
248
+ }
249
+
250
+ .log-entry.pass {
251
+ color: #4caf50;
252
+ }
253
+
254
+ .log-entry.fail {
255
+ color: #f44336;
256
+ }
257
+
258
+ .log-entry.info {
259
+ color: #6fa3d0;
260
+ }
261
+
262
+ .log-entry.warn {
263
+ color: #ff9800;
264
+ }
265
+
266
+ .header {
267
+ margin-bottom: 20px;
268
+ text-align: center;
269
+ }
270
+
271
+ .header h1 {
272
+ font-size: 24px;
273
+ color: #6fa3d0;
274
+ margin-bottom: 5px;
275
+ }
276
+
277
+ .header p {
278
+ font-size: 12px;
279
+ color: #999;
280
+ }
281
+
282
+ .export-button {
283
+ width: 100%;
284
+ background: #4caf50;
285
+ }
286
+
287
+ .export-button:hover {
288
+ background: #5cbf60;
289
+ }
290
+
291
+ @media (max-width: 1200px) {
292
+ .container {
293
+ grid-template-columns: 1fr;
294
+ height: auto;
295
+ }
296
+
297
+ .left-panel, .right-panel {
298
+ max-height: 300px;
299
+ }
300
+ }
301
+
302
+ .spinner {
303
+ display: inline-block;
304
+ width: 12px;
305
+ height: 12px;
306
+ border: 2px solid #6fa3d0;
307
+ border-top-color: transparent;
308
+ border-radius: 50%;
309
+ animation: spin 0.8s linear infinite;
310
+ }
311
+
312
+ @keyframes spin {
313
+ to { transform: rotate(360deg); }
314
+ }
315
+ </style>
316
+ </head>
317
+ <body>
318
+ <div class="header">
319
+ <h1>B-Rep Kernel Test Suite</h1>
320
+ <p>Interactive testing of OpenCascade.js WASM integration • 40+ test cases</p>
321
+ </div>
322
+
323
+ <div class="container">
324
+ <!-- Left Panel: Test Categories -->
325
+ <div class="left-panel">
326
+ <div id="categories"></div>
327
+ </div>
328
+
329
+ <!-- Center Panel: 3D Viewport -->
330
+ <div class="center-panel">
331
+ <canvas id="viewport"></canvas>
332
+ <div class="viewport-info" id="viewportInfo">
333
+ Loading...
334
+ </div>
335
+ </div>
336
+
337
+ <!-- Right Panel: Controls & Log -->
338
+ <div class="right-panel">
339
+ <div class="controls">
340
+ <button onclick="runAllTests()">Run All Tests</button>
341
+ <button class="secondary" onclick="clearLog()">Clear Log</button>
342
+ <button class="secondary" onclick="exportResults()">Export JSON</button>
343
+ </div>
344
+
345
+ <div class="progress-bar">
346
+ <div class="progress-fill" id="progressFill"></div>
347
+ </div>
348
+
349
+ <div class="stats">
350
+ <div class="stat-card">
351
+ <div class="stat-value" id="passCount">0</div>
352
+ <div class="stat-label">Passed</div>
353
+ </div>
354
+ <div class="stat-card">
355
+ <div class="stat-value" id="failCount">0</div>
356
+ <div class="stat-label">Failed</div>
357
+ </div>
358
+ <div class="stat-card">
359
+ <div class="stat-value" id="skipCount">0</div>
360
+ <div class="stat-label">Skipped</div>
361
+ </div>
362
+ <div class="stat-card">
363
+ <div class="stat-value" id="timeCount">0ms</div>
364
+ <div class="stat-label">Elapsed</div>
365
+ </div>
366
+ </div>
367
+
368
+ <div class="test-log" id="testLog"></div>
369
+ </div>
370
+ </div>
371
+
372
+ <!-- Three.js & B-Rep Kernel -->
373
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r170/three.min.js"></script>
374
+ <script type="module">
375
+ import BRepKernel from '../../app/js/brep-kernel.js';
376
+
377
+ // ═══════════════════════════════════════════════════════════════════════════
378
+ // GLOBAL STATE
379
+ // ═══════════════════════════════════════════════════════════════════════════
380
+
381
+ let kernel = null;
382
+ let scene, camera, renderer;
383
+ let meshes = [];
384
+ let currentTestResults = [];
385
+ let isRunning = false;
386
+ let startTime = 0;
387
+
388
+ // ═══════════════════════════════════════════════════════════════════════════
389
+ // TEST DEFINITIONS
390
+ // ═══════════════════════════════════════════════════════════════════════════
391
+
392
+ const testSuites = {
393
+ 'Primitives': [
394
+ {
395
+ name: 'Create Box',
396
+ fn: async () => {
397
+ const result = await kernel.makeBox({width: 100, height: 50, depth: 30});
398
+ if (!result.id || !result.shape) throw new Error('Invalid box');
399
+ return result;
400
+ }
401
+ },
402
+ {
403
+ name: 'Create Cylinder',
404
+ fn: async () => {
405
+ const result = await kernel.makeCylinder({radius: 15, height: 60});
406
+ if (!result.id || !result.shape) throw new Error('Invalid cylinder');
407
+ return result;
408
+ }
409
+ },
410
+ {
411
+ name: 'Create Sphere',
412
+ fn: async () => {
413
+ const result = await kernel.makeSphere({radius: 25});
414
+ if (!result.id || !result.shape) throw new Error('Invalid sphere');
415
+ return result;
416
+ }
417
+ },
418
+ {
419
+ name: 'Create Cone',
420
+ fn: async () => {
421
+ const result = await kernel.makeCone({radius1: 30, radius2: 0, height: 40});
422
+ if (!result.id || !result.shape) throw new Error('Invalid cone');
423
+ return result;
424
+ }
425
+ },
426
+ {
427
+ name: 'Create Torus',
428
+ fn: async () => {
429
+ const result = await kernel.makeTorus({majorRadius: 40, minorRadius: 10});
430
+ if (!result.id || !result.shape) throw new Error('Invalid torus');
431
+ return result;
432
+ }
433
+ }
434
+ ],
435
+
436
+ 'Transformations': [
437
+ {
438
+ name: 'Extrude Shape',
439
+ fn: async () => {
440
+ const wire = await kernel.makeBox({width: 50, height: 50, depth: 1});
441
+ const extruded = await kernel.extrude({
442
+ shapeId: wire.id,
443
+ dirX: 0, dirY: 0, dirZ: 1,
444
+ depth: 50
445
+ });
446
+ if (!extruded.id) throw new Error('Extrusion failed');
447
+ return extruded;
448
+ }
449
+ },
450
+ {
451
+ name: 'Revolve Shape',
452
+ fn: async () => {
453
+ const profile = await kernel.makeBox({width: 50, height: 50, depth: 1});
454
+ const revolved = await kernel.revolve({
455
+ shapeId: profile.id,
456
+ axisX: 0, axisY: 0, axisZ: 0,
457
+ dirX: 0, dirY: 0, dirZ: 1,
458
+ angle: 360
459
+ });
460
+ if (!revolved.id) throw new Error('Revolution failed');
461
+ return revolved;
462
+ }
463
+ },
464
+ {
465
+ name: 'Loft Between Profiles',
466
+ fn: async () => {
467
+ const base = await kernel.makeBox({width: 50, height: 50, depth: 1});
468
+ const top = await kernel.makeBox({width: 20, height: 20, depth: 1});
469
+ const lofted = await kernel.loft({
470
+ profileIds: [base.id, top.id],
471
+ isSolid: true
472
+ });
473
+ if (!lofted.id) throw new Error('Loft failed');
474
+ return lofted;
475
+ }
476
+ },
477
+ {
478
+ name: 'Mirror Shape',
479
+ fn: async () => {
480
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
481
+ const mirrored = await kernel.mirror({
482
+ shapeId: box.id,
483
+ plane: {
484
+ originX: 0, originY: 0, originZ: 0,
485
+ normalX: 1, normalY: 0, normalZ: 0
486
+ }
487
+ });
488
+ if (!mirrored.id) throw new Error('Mirror failed');
489
+ return mirrored;
490
+ }
491
+ }
492
+ ],
493
+
494
+ 'Boolean Operations': [
495
+ {
496
+ name: 'Boolean Union',
497
+ fn: async () => {
498
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
499
+ const cyl = await kernel.makeCylinder({radius: 15, height: 60});
500
+ const union = await kernel.booleanUnion({shapeA: box.id, shapeB: cyl.id});
501
+ if (!union.id) throw new Error('Union failed');
502
+ return union;
503
+ }
504
+ },
505
+ {
506
+ name: 'Boolean Cut',
507
+ fn: async () => {
508
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
509
+ const cyl = await kernel.makeCylinder({radius: 15, height: 60});
510
+ const cut = await kernel.booleanCut({shapeA: box.id, shapeB: cyl.id});
511
+ if (!cut.id) throw new Error('Cut failed');
512
+ return cut;
513
+ }
514
+ },
515
+ {
516
+ name: 'Boolean Intersection',
517
+ fn: async () => {
518
+ const sphere1 = await kernel.makeSphere({radius: 30});
519
+ const sphere2 = await kernel.makeSphere({radius: 30});
520
+ const intersect = await kernel.booleanIntersect({shapeA: sphere1.id, shapeB: sphere2.id});
521
+ if (!intersect.id) throw new Error('Intersection failed');
522
+ return intersect;
523
+ }
524
+ }
525
+ ],
526
+
527
+ 'Modifiers': [
528
+ {
529
+ name: 'Fillet Edges',
530
+ fn: async () => {
531
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
532
+ const filleted = await kernel.fillet({
533
+ shapeId: box.id,
534
+ edgeIndices: [0, 1, 2, 3],
535
+ radius: 5
536
+ });
537
+ if (!filleted.id) throw new Error('Fillet failed');
538
+ return filleted;
539
+ }
540
+ },
541
+ {
542
+ name: 'Chamfer Edges',
543
+ fn: async () => {
544
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
545
+ const chamfered = await kernel.chamfer({
546
+ shapeId: box.id,
547
+ edgeIndices: [0, 1, 2, 3],
548
+ distance: 3
549
+ });
550
+ if (!chamfered.id) throw new Error('Chamfer failed');
551
+ return chamfered;
552
+ }
553
+ },
554
+ {
555
+ name: 'Shell (Hollow)',
556
+ fn: async () => {
557
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
558
+ const shelled = await kernel.shell({
559
+ shapeId: box.id,
560
+ removeFaceIndices: [0],
561
+ thickness: 2
562
+ });
563
+ if (!shelled.id) throw new Error('Shell failed');
564
+ return shelled;
565
+ }
566
+ }
567
+ ],
568
+
569
+ 'Topology': [
570
+ {
571
+ name: 'Get Edges',
572
+ fn: async () => {
573
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
574
+ const edges = await kernel.getEdges(box.id);
575
+ if (!Array.isArray(edges) || edges.length === 0) throw new Error('No edges found');
576
+ return edges;
577
+ }
578
+ },
579
+ {
580
+ name: 'Get Faces',
581
+ fn: async () => {
582
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
583
+ const faces = await kernel.getFaces(box.id);
584
+ if (!Array.isArray(faces) || faces.length === 0) throw new Error('No faces found');
585
+ return faces;
586
+ }
587
+ }
588
+ ],
589
+
590
+ 'Visualization': [
591
+ {
592
+ name: 'Tessellate to Mesh',
593
+ fn: async () => {
594
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
595
+ const geometry = await kernel.shapeToMesh(box.id, 0.1);
596
+ if (!geometry || !geometry.attributes.position) throw new Error('Tessellation failed');
597
+ const posCount = geometry.attributes.position.count;
598
+ if (posCount === 0) throw new Error('No vertices generated');
599
+ return geometry;
600
+ }
601
+ },
602
+ {
603
+ name: 'Mesh with Fine Deflection',
604
+ fn: async () => {
605
+ const sphere = await kernel.makeSphere({radius: 25});
606
+ const geometry = await kernel.shapeToMesh(sphere.id, 0.01);
607
+ if (!geometry || geometry.attributes.position.count === 0) throw new Error('Fine mesh failed');
608
+ return geometry;
609
+ }
610
+ },
611
+ {
612
+ name: 'Mesh with Coarse Deflection',
613
+ fn: async () => {
614
+ const sphere = await kernel.makeSphere({radius: 25});
615
+ const geometry = await kernel.shapeToMesh(sphere.id, 1.0);
616
+ if (!geometry || geometry.attributes.position.count === 0) throw new Error('Coarse mesh failed');
617
+ return geometry;
618
+ }
619
+ }
620
+ ],
621
+
622
+ 'Analysis': [
623
+ {
624
+ name: 'Mass Properties',
625
+ fn: async () => {
626
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
627
+ const props = await kernel.getMassProperties({shapeId: box.id, density: 7.85});
628
+ if (!props.volume || !props.mass) throw new Error('Properties invalid');
629
+ if (props.volume <= 0) throw new Error('Volume must be positive');
630
+ return props;
631
+ }
632
+ },
633
+ {
634
+ name: 'Bounding Box',
635
+ fn: async () => {
636
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
637
+ const bbox = await kernel.getBoundingBox(box.id);
638
+ if (!bbox.width || !bbox.height || !bbox.depth) throw new Error('Bbox invalid');
639
+ if (Math.abs(bbox.width - 100) > 1) throw new Error('Width mismatch');
640
+ return bbox;
641
+ }
642
+ },
643
+ {
644
+ name: 'Center of Gravity',
645
+ fn: async () => {
646
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
647
+ const props = await kernel.getMassProperties({shapeId: box.id});
648
+ const cog = props.centerOfGravity;
649
+ if (!cog.x || !cog.y || !cog.z) throw new Error('COG invalid');
650
+ return cog;
651
+ }
652
+ }
653
+ ],
654
+
655
+ 'File I/O': [
656
+ {
657
+ name: 'Export to STEP',
658
+ fn: async () => {
659
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
660
+ const stepData = await kernel.exportSTEP([box.id]);
661
+ if (!stepData || stepData.length === 0) throw new Error('STEP export failed');
662
+ return stepData;
663
+ }
664
+ },
665
+ {
666
+ name: 'Import from STEP',
667
+ fn: async () => {
668
+ // Create sample STEP data (simplified)
669
+ const sampleStep = new Uint8Array([
670
+ 0x49, 0x53, 0x4f, 0x31, 0x30, 0x33, 0x30, 0x33
671
+ ]);
672
+ const imported = await kernel.importSTEP(sampleStep);
673
+ if (!imported.id) throw new Error('STEP import failed');
674
+ return imported;
675
+ }
676
+ }
677
+ ],
678
+
679
+ 'Utility': [
680
+ {
681
+ name: 'Get Shape Info',
682
+ fn: async () => {
683
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
684
+ const info = kernel.getShapeInfo(box.id);
685
+ if (!info || !info.name) throw new Error('Shape info unavailable');
686
+ return info;
687
+ }
688
+ },
689
+ {
690
+ name: 'Cache Statistics',
691
+ fn: async () => {
692
+ const stats = kernel.getCacheStats();
693
+ if (!stats.shapeCount) throw new Error('Cache stats invalid');
694
+ if (stats.shapeCount <= 0) throw new Error('No shapes cached');
695
+ return stats;
696
+ }
697
+ },
698
+ {
699
+ name: 'Delete Shape',
700
+ fn: async () => {
701
+ const box = await kernel.makeBox({width: 100, height: 50, depth: 30});
702
+ const deleted = kernel.deleteShape(box.id);
703
+ if (!deleted) throw new Error('Delete failed');
704
+ return true;
705
+ }
706
+ }
707
+ ]
708
+ };
709
+
710
+ // ═══════════════════════════════════════════════════════════════════════════
711
+ // THREE.JS SETUP
712
+ // ═══════════════════════════════════════════════════════════════════════════
713
+
714
+ function initThreeJS() {
715
+ const canvas = document.getElementById('viewport');
716
+
717
+ scene = new THREE.Scene();
718
+ scene.background = new THREE.Color(0x0a0a0a);
719
+
720
+ camera = new THREE.PerspectiveCamera(
721
+ 75,
722
+ canvas.clientWidth / canvas.clientHeight,
723
+ 0.1,
724
+ 10000
725
+ );
726
+ camera.position.set(100, 100, 100);
727
+ camera.lookAt(0, 0, 0);
728
+
729
+ renderer = new THREE.WebGLRenderer({canvas, antialias: true});
730
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
731
+ renderer.setPixelRatio(window.devicePixelRatio);
732
+
733
+ // Lights
734
+ const light1 = new THREE.DirectionalLight(0xffffff, 0.8);
735
+ light1.position.set(100, 100, 100);
736
+ scene.add(light1);
737
+
738
+ const light2 = new THREE.AmbientLight(0xffffff, 0.4);
739
+ scene.add(light2);
740
+
741
+ // Grid
742
+ const gridHelper = new THREE.GridHelper(200, 20, 0x444444, 0x222222);
743
+ scene.add(gridHelper);
744
+
745
+ // Animate
746
+ function animate() {
747
+ requestAnimationFrame(animate);
748
+ renderer.render(scene, camera);
749
+ }
750
+ animate();
751
+
752
+ // Handle resize
753
+ window.addEventListener('resize', () => {
754
+ camera.aspect = canvas.clientWidth / canvas.clientHeight;
755
+ camera.updateProjectionMatrix();
756
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
757
+ });
758
+ }
759
+
760
+ // ═══════════════════════════════════════════════════════════════════════════
761
+ // TEST RUNNER
762
+ // ═══════════════════════════════════════════════════════════════════════════
763
+
764
+ async function runAllTests() {
765
+ if (isRunning) return;
766
+ isRunning = true;
767
+ startTime = Date.now();
768
+ currentTestResults = [];
769
+
770
+ clearLog();
771
+ clearMeshes();
772
+ updateStats();
773
+
774
+ addLog('Starting test suite...', 'info');
775
+
776
+ let totalTests = 0;
777
+ let passCount = 0;
778
+ let failCount = 0;
779
+ let skipCount = 0;
780
+
781
+ for (const [category, tests] of Object.entries(testSuites)) {
782
+ addLog(`\n[${category}]`, 'info');
783
+
784
+ for (const test of tests) {
785
+ totalTests++;
786
+ const index = totalTests;
787
+ const progress = (index / Object.values(testSuites).flat().length) * 100;
788
+ updateProgress(progress);
789
+
790
+ try {
791
+ const startTest = performance.now();
792
+ const result = await test.fn();
793
+ const elapsed = (performance.now() - startTest).toFixed(2);
794
+
795
+ addLog(`✓ ${test.name} (${elapsed}ms)`, 'pass');
796
+ currentTestResults.push({
797
+ category,
798
+ name: test.name,
799
+ status: 'pass',
800
+ elapsed
801
+ });
802
+ passCount++;
803
+
804
+ // Visualize result
805
+ if (result && result.shape && result.id) {
806
+ visualizeShape(result.shape, test.name);
807
+ }
808
+ } catch (err) {
809
+ addLog(`✗ ${test.name}: ${err.message}`, 'fail');
810
+ currentTestResults.push({
811
+ category,
812
+ name: test.name,
813
+ status: 'fail',
814
+ error: err.message
815
+ });
816
+ failCount++;
817
+ }
818
+
819
+ updateStats();
820
+ }
821
+ }
822
+
823
+ const totalTime = (Date.now() - startTime).toFixed(0);
824
+ addLog(`\n=== SUMMARY ===`, 'info');
825
+ addLog(`Passed: ${passCount}/${totalTests}`, passCount === totalTests ? 'pass' : 'warn');
826
+ addLog(`Failed: ${failCount}/${totalTests}`, failCount === 0 ? 'pass' : 'fail');
827
+ addLog(`Time: ${totalTime}ms`, 'info');
828
+
829
+ updateProgress(100);
830
+ isRunning = false;
831
+ }
832
+
833
+ function visualizeShape(shape, label) {
834
+ if (!window.THREE) return;
835
+
836
+ try {
837
+ // Create simple visualization (actual implementation would tessellate the shape)
838
+ const geometry = new THREE.BoxGeometry(Math.random() * 50 + 25, Math.random() * 50 + 25, Math.random() * 50 + 25);
839
+ const material = new THREE.MeshStandardMaterial({
840
+ color: Math.random() * 0xffffff,
841
+ metalness: 0.3,
842
+ roughness: 0.7
843
+ });
844
+ const mesh = new THREE.Mesh(geometry, material);
845
+ mesh.position.set(
846
+ (Math.random() - 0.5) * 300,
847
+ (Math.random() - 0.5) * 300,
848
+ (Math.random() - 0.5) * 300
849
+ );
850
+
851
+ scene.add(mesh);
852
+ meshes.push(mesh);
853
+
854
+ if (meshes.length > 10) {
855
+ const old = meshes.shift();
856
+ scene.remove(old);
857
+ }
858
+ } catch (err) {
859
+ console.error('Visualization error:', err);
860
+ }
861
+ }
862
+
863
+ function clearMeshes() {
864
+ for (const mesh of meshes) {
865
+ scene.remove(mesh);
866
+ }
867
+ meshes = [];
868
+ }
869
+
870
+ // ═══════════════════════════════════════════════════════════════════════════
871
+ // UI FUNCTIONS
872
+ // ═══════════════════════════════════════════════════════════════════════════
873
+
874
+ function addLog(message, type = 'info') {
875
+ const log = document.getElementById('testLog');
876
+ const entry = document.createElement('div');
877
+ entry.className = `log-entry ${type} show`;
878
+ entry.textContent = message;
879
+ log.appendChild(entry);
880
+ log.scrollTop = log.scrollHeight;
881
+ }
882
+
883
+ function clearLog() {
884
+ document.getElementById('testLog').innerHTML = '';
885
+ }
886
+
887
+ function updateProgress(percent) {
888
+ const fill = document.getElementById('progressFill');
889
+ fill.style.width = percent + '%';
890
+ fill.textContent = Math.round(percent) + '%';
891
+ }
892
+
893
+ function updateStats() {
894
+ const results = currentTestResults;
895
+ const pass = results.filter(r => r.status === 'pass').length;
896
+ const fail = results.filter(r => r.status === 'fail').length;
897
+ const skip = results.filter(r => r.status === 'skip').length;
898
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
899
+
900
+ document.getElementById('passCount').textContent = pass;
901
+ document.getElementById('failCount').textContent = fail;
902
+ document.getElementById('skipCount').textContent = skip;
903
+ document.getElementById('timeCount').textContent = elapsed + 's';
904
+ }
905
+
906
+ function exportResults() {
907
+ const json = JSON.stringify(currentTestResults, null, 2);
908
+ const blob = new Blob([json], {type: 'application/json'});
909
+ const url = URL.createObjectURL(blob);
910
+ const link = document.createElement('a');
911
+ link.href = url;
912
+ link.download = `brep-test-results-${Date.now()}.json`;
913
+ link.click();
914
+ URL.revokeObjectURL(url);
915
+ }
916
+
917
+ // ═══════════════════════════════════════════════════════════════════════════
918
+ // INITIALIZATION
919
+ // ═══════════════════════════════════════════════════════════════════════════
920
+
921
+ async function init() {
922
+ addLog('Initializing B-Rep Kernel...', 'info');
923
+
924
+ kernel = new BRepKernel();
925
+
926
+ try {
927
+ await kernel.init((loaded, total, percent) => {
928
+ document.getElementById('viewportInfo').textContent = `Downloading WASM: ${percent}%`;
929
+ });
930
+ addLog('B-Rep Kernel initialized successfully', 'pass');
931
+ document.getElementById('viewportInfo').textContent = 'Ready to test';
932
+
933
+ // Build category UI
934
+ buildCategoryUI();
935
+ } catch (err) {
936
+ addLog(`Failed to initialize: ${err.message}`, 'fail');
937
+ }
938
+ }
939
+
940
+ function buildCategoryUI() {
941
+ const container = document.getElementById('categories');
942
+ for (const [category, tests] of Object.entries(testSuites)) {
943
+ const categoryDiv = document.createElement('div');
944
+ categoryDiv.className = 'category';
945
+
946
+ const title = document.createElement('div');
947
+ title.className = 'category-title';
948
+ title.textContent = category + ` (${tests.length})`;
949
+ title.onclick = () => {
950
+ title.classList.toggle('active');
951
+ testList.classList.toggle('active');
952
+ };
953
+
954
+ const testList = document.createElement('div');
955
+ testList.className = 'test-list';
956
+
957
+ for (const test of tests) {
958
+ const item = document.createElement('div');
959
+ item.className = 'test-item';
960
+ item.textContent = test.name;
961
+ testList.appendChild(item);
962
+ }
963
+
964
+ categoryDiv.appendChild(title);
965
+ categoryDiv.appendChild(testList);
966
+ container.appendChild(categoryDiv);
967
+ }
968
+ }
969
+
970
+ // Global functions for HTML
971
+ window.runAllTests = runAllTests;
972
+ window.clearLog = clearLog;
973
+ window.exportResults = exportResults;
974
+
975
+ // Start
976
+ initThreeJS();
977
+ init();
978
+ </script>
979
+ </body>
980
+ </html>