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.
- package/CLAUDE.md +155 -1
- package/DOCKER-SETUP-VERIFICATION.md +399 -0
- package/DOCKER-TESTING.md +463 -0
- package/FUSION360_MODULES.md +478 -0
- package/FUSION_MODULES_README.md +352 -0
- package/INTEGRATION_SNIPPETS.md +608 -0
- package/KILLER-FEATURES-DELIVERY.md +469 -0
- package/MODULES_SUMMARY.txt +337 -0
- package/QUICK_REFERENCE.txt +298 -0
- package/README-DOCKER-TESTING.txt +438 -0
- package/app/index.html +23 -10
- package/app/js/fusion-help.json +1808 -0
- package/app/js/help-module-v3.js +1096 -0
- package/app/js/killer-features-help.json +395 -0
- package/app/js/killer-features.js +1508 -0
- package/app/js/modules/fusion-assembly.js +842 -0
- package/app/js/modules/fusion-cam.js +785 -0
- package/app/js/modules/fusion-data.js +814 -0
- package/app/js/modules/fusion-drawing.js +844 -0
- package/app/js/modules/fusion-inspection.js +756 -0
- package/app/js/modules/fusion-render.js +774 -0
- package/app/js/modules/fusion-simulation.js +986 -0
- package/app/js/modules/fusion-sketch.js +1044 -0
- package/app/js/modules/fusion-solid.js +1095 -0
- package/app/js/modules/fusion-surface.js +949 -0
- package/app/tests/FUSION_TEST_SUITE.md +266 -0
- package/app/tests/README.md +77 -0
- package/app/tests/TESTING-CHECKLIST.md +177 -0
- package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
- package/app/tests/brep-live-test.html +848 -0
- package/app/tests/docker-integration-test.html +811 -0
- package/app/tests/fusion-all-tests.html +670 -0
- package/app/tests/fusion-assembly-tests.html +461 -0
- package/app/tests/fusion-cam-tests.html +421 -0
- package/app/tests/fusion-simulation-tests.html +421 -0
- package/app/tests/fusion-sketch-tests.html +613 -0
- package/app/tests/fusion-solid-tests.html +529 -0
- package/app/tests/index.html +453 -0
- package/app/tests/killer-features-test.html +509 -0
- package/app/tests/run-tests.html +874 -0
- package/app/tests/step-import-live-test.html +1115 -0
- package/app/tests/test-agent-v3.html +93 -696
- package/architecture-dashboard.html +1970 -0
- package/docs/API-REFERENCE.md +1423 -0
- package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
- package/docs/DEVELOPER-GUIDE-v3.md +795 -0
- package/docs/DOCKER-QUICK-TEST.md +376 -0
- package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
- package/docs/FUSION-TUTORIAL.md +1203 -0
- package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
- package/docs/KEYBOARD-SHORTCUTS.md +402 -0
- package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
- package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
- package/docs/KILLER-FEATURES.md +562 -0
- package/docs/QUICK-REFERENCE.md +282 -0
- package/docs/README-v3-DOCS.md +274 -0
- package/docs/TUTORIAL-v3.md +1190 -0
- package/docs/architecture-dashboard.html +1970 -0
- package/docs/architecture-v3.html +1038 -0
- package/linkedin-post-v3.md +58 -0
- package/package.json +1 -1
- package/scripts/dev-setup.sh +338 -0
- package/scripts/docker-health-check.sh +159 -0
- package/scripts/integration-test.sh +311 -0
- 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>
|