cyclecad 3.0.0 → 3.2.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/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
- package/BILLING-INDEX.md +293 -0
- package/BILLING-INTEGRATION-GUIDE.md +414 -0
- package/COLLABORATION-INDEX.md +440 -0
- package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
- package/DOCKER-BUILD-MANIFEST.txt +483 -0
- package/DOCKER-FILES-REFERENCE.md +440 -0
- package/DOCKER-INFRASTRUCTURE.md +475 -0
- package/DOCKER-README.md +435 -0
- package/Dockerfile +33 -55
- package/PWA-FILES-CREATED.txt +350 -0
- package/QUICK-START-TESTING.md +126 -0
- package/STEP-IMPORT-QUICKSTART.md +347 -0
- package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
- package/app/css/mobile.css +1074 -0
- package/app/icons/generate-icons.js +203 -0
- package/app/index.html +93 -0
- package/app/js/billing-ui.js +990 -0
- package/app/js/brep-kernel.js +933 -981
- package/app/js/collab-client.js +750 -0
- package/app/js/mobile-nav.js +623 -0
- package/app/js/mobile-toolbar.js +476 -0
- package/app/js/modules/billing-module.js +724 -0
- package/app/js/modules/step-module-enhanced.js +938 -0
- package/app/js/offline-manager.js +705 -0
- package/app/js/responsive-init.js +360 -0
- package/app/js/touch-handler.js +429 -0
- package/app/manifest.json +211 -0
- package/app/offline.html +508 -0
- package/app/sw.js +571 -0
- package/app/tests/billing-tests.html +779 -0
- package/app/tests/brep-tests.html +980 -0
- package/app/tests/collab-tests.html +743 -0
- package/app/tests/mobile-tests.html +1299 -0
- package/app/tests/pwa-tests.html +1134 -0
- package/app/tests/step-tests.html +1042 -0
- package/app/tests/test-agent-v3.html +719 -0
- package/docker-compose.yml +225 -0
- package/docs/BILLING-HELP.json +260 -0
- package/docs/BILLING-README.md +639 -0
- package/docs/BILLING-TUTORIAL.md +736 -0
- package/docs/BREP-HELP.json +326 -0
- package/docs/BREP-TUTORIAL.md +802 -0
- package/docs/COLLABORATION-HELP.json +228 -0
- package/docs/COLLABORATION-TUTORIAL.md +818 -0
- package/docs/DOCKER-HELP.json +224 -0
- package/docs/DOCKER-TUTORIAL.md +974 -0
- package/docs/MOBILE-HELP.json +243 -0
- package/docs/MOBILE-RESPONSIVE-README.md +378 -0
- package/docs/MOBILE-TUTORIAL.md +747 -0
- package/docs/PWA-HELP.json +228 -0
- package/docs/PWA-README.md +662 -0
- package/docs/PWA-TUTORIAL.md +757 -0
- package/docs/STEP-HELP.json +481 -0
- package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
- package/docs/TESTING-GUIDE.md +528 -0
- package/docs/TESTING-HELP.json +182 -0
- package/fusion-vs-cyclecad.html +1771 -0
- package/nginx.conf +237 -0
- package/package.json +1 -1
- package/server/Dockerfile.converter +51 -0
- package/server/Dockerfile.signaling +28 -0
- package/server/billing-server.js +487 -0
- package/server/converter-enhanced.py +528 -0
- package/server/requirements-converter.txt +29 -0
- package/server/signaling-server.js +801 -0
- package/tests/docker-tests.sh +389 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file step-module-enhanced.js
|
|
3
|
+
* @description Enhanced STEP File Import/Export with Multi-Strategy Router
|
|
4
|
+
* Intelligent routing for STEP files of any size:
|
|
5
|
+
* - Small files (<30MB) → Browser WASM (occt-import-js)
|
|
6
|
+
* - Medium files (30-50MB) → OpenCascade.js Worker (full B-rep)
|
|
7
|
+
* - Large files (≥50MB) → Server-side Python converter (FastAPI)
|
|
8
|
+
* - XL files (>100MB) → Warnings + chunking suggestions
|
|
9
|
+
*
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
* @author cycleCAD Team
|
|
12
|
+
* @license MIT
|
|
13
|
+
* @see {@link https://github.com/vvlars-cmd/cyclecad}
|
|
14
|
+
*
|
|
15
|
+
* @module step-module-enhanced
|
|
16
|
+
* @requires viewport (3D scene management)
|
|
17
|
+
*
|
|
18
|
+
* Key Improvements Over v1.0.0:
|
|
19
|
+
* 1. Multi-strategy routing (occt-import-js → OpenCascade.js → Server)
|
|
20
|
+
* 2. Worker heartbeat with 90s timeout + user-friendly error messages
|
|
21
|
+
* 3. `.slice(0)` tight loop to copy ALL WASM data BEFORE postMessage
|
|
22
|
+
* 4. GLB caching in IndexedDB with file hash invalidation
|
|
23
|
+
* 5. Adaptive deflection (coarser triangles for larger files)
|
|
24
|
+
* 6. Progress tracking with percentage + elapsed time
|
|
25
|
+
* 7. Cancel button to abort long-running imports
|
|
26
|
+
* 8. Server-side metadata endpoint (no full parse needed)
|
|
27
|
+
* 9. Color preservation from STEP file geometry
|
|
28
|
+
* 10. Comprehensive error handling with recovery suggestions
|
|
29
|
+
*
|
|
30
|
+
* Import Pipeline:
|
|
31
|
+
* 1. User selects STEP file or URL
|
|
32
|
+
* 2. Check file size and compute file hash (MD5 for cache key)
|
|
33
|
+
* 3. Try to load from IndexedDB cache (hit = instant load)
|
|
34
|
+
* 4. If cache miss or invalidated:
|
|
35
|
+
* a. File size < 30MB → Router selects occt-import-js Worker
|
|
36
|
+
* b. File size 30-50MB → Router selects OpenCascade.js Worker (if available)
|
|
37
|
+
* c. File size ≥ 50MB → Router selects Server converter
|
|
38
|
+
* 5. Parser returns mesh data (position, normal, index, color, name)
|
|
39
|
+
* 6. Create Three.js geometry and add to scene
|
|
40
|
+
* 7. Save GLB to cache with file hash + metadata
|
|
41
|
+
* 8. Emit events: 'step:importStart' → 'step:importProgress' → 'step:importComplete'
|
|
42
|
+
*
|
|
43
|
+
* Worker Architecture (Blob URL Pattern):
|
|
44
|
+
* - occt-import-js Worker: ~100KB inline, handles <30MB files, ~2-10s parse time
|
|
45
|
+
* - OpenCascade.js Worker: ~50MB WASM (loaded from CDN), handles 30-50MB files
|
|
46
|
+
* - Both workers send heartbeat every 5s
|
|
47
|
+
* - Main thread monitors for 90s silence → terminate + error
|
|
48
|
+
*
|
|
49
|
+
* Server Conversion:
|
|
50
|
+
* - Configurable endpoint (default: http://localhost:8787/convert)
|
|
51
|
+
* - POST /convert: Upload STEP → Get GLB back
|
|
52
|
+
* - GET /convert/metadata: Quick metadata (part count, names)
|
|
53
|
+
* - GET /convert/health: Server + WASM status
|
|
54
|
+
* - Supports Docker deployment with memory limits
|
|
55
|
+
* - Returns glTF 2.0 binary format (GLB) for easy loading
|
|
56
|
+
*
|
|
57
|
+
* Caching Strategy:
|
|
58
|
+
* - IndexedDB key: `step-glb-{file-hash}-{deflection}`
|
|
59
|
+
* - Invalidates on file change (hash mismatch)
|
|
60
|
+
* - Metadata: { fileName, fileSize, fileHash, parseTime, deflection, timestamp }
|
|
61
|
+
* - Auto-cleanup on quota exceeded (LRU eviction)
|
|
62
|
+
*
|
|
63
|
+
* Error Recovery:
|
|
64
|
+
* - WASM timeout (90s) → Terminate worker, suggest server converter
|
|
65
|
+
* - Server error (5xx) → Fallback to browser WASM (if file <50MB)
|
|
66
|
+
* - Corrupt file → Show hex dump of first 512 bytes for debugging
|
|
67
|
+
* - Memory pressure → Suggest file splitting at assembly boundaries
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
const StepModuleEnhanced = {
|
|
71
|
+
id: 'step-io-enhanced',
|
|
72
|
+
name: 'STEP Import/Export (Enhanced)',
|
|
73
|
+
version: '2.0.0',
|
|
74
|
+
category: 'data',
|
|
75
|
+
dependencies: ['viewport'],
|
|
76
|
+
memoryEstimate: 120, // WASM + caching
|
|
77
|
+
|
|
78
|
+
// ========== MODULE STATE ==========
|
|
79
|
+
state: {
|
|
80
|
+
importInProgress: false,
|
|
81
|
+
importCanceled: false,
|
|
82
|
+
importProgress: 0,
|
|
83
|
+
workerReady: false,
|
|
84
|
+
opencascadeReady: false,
|
|
85
|
+
serverURL: 'http://localhost:8787/convert',
|
|
86
|
+
serverHealthy: false,
|
|
87
|
+
useOpenCascade: false,
|
|
88
|
+
useBrepKernel: false,
|
|
89
|
+
lastImportInfo: null,
|
|
90
|
+
cacheEnabled: true,
|
|
91
|
+
deflectionDefaults: {
|
|
92
|
+
small: 0.01, // <30MB: fine detail
|
|
93
|
+
medium: 0.02, // 30-50MB: balanced
|
|
94
|
+
large: 0.05, // 50-100MB: coarse
|
|
95
|
+
xlarge: 0.1, // >100MB: very coarse
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
worker: null,
|
|
100
|
+
opencascadeWorker: null,
|
|
101
|
+
workerHeartbeat: null,
|
|
102
|
+
importAbortController: null,
|
|
103
|
+
kernel: null,
|
|
104
|
+
db: null,
|
|
105
|
+
|
|
106
|
+
// ========== INITIALIZATION ==========
|
|
107
|
+
async init(kernel) {
|
|
108
|
+
this.kernel = kernel;
|
|
109
|
+
this.state.serverURL = localStorage.getItem('ev_converter_url') || 'http://localhost:8787/convert';
|
|
110
|
+
this.state.cacheEnabled = localStorage.getItem('ev_step_cache_enabled') !== 'false';
|
|
111
|
+
|
|
112
|
+
// Initialize IndexedDB cache
|
|
113
|
+
this.initCache();
|
|
114
|
+
|
|
115
|
+
// Check server health
|
|
116
|
+
this.checkServerHealth();
|
|
117
|
+
|
|
118
|
+
// Initialize Web Workers
|
|
119
|
+
this.initWorkers();
|
|
120
|
+
|
|
121
|
+
// Check for B-Rep kernel
|
|
122
|
+
const brepModule = kernel.modules?.find(m => m.id === 'brep-kernel');
|
|
123
|
+
this.state.useBrepKernel = !!brepModule;
|
|
124
|
+
|
|
125
|
+
// Register commands
|
|
126
|
+
kernel.registerCommand('step.import', (file) => this.import(file));
|
|
127
|
+
kernel.registerCommand('step.export', (filename) => this.export(filename));
|
|
128
|
+
kernel.registerCommand('step.importFromURL', (url) => this.importFromURL(url));
|
|
129
|
+
kernel.registerCommand('step.getMetadata', (file) => this.getMetadata(file));
|
|
130
|
+
kernel.registerCommand('step.setServerURL', (url) => this.setServerURL(url));
|
|
131
|
+
kernel.registerCommand('step.clearCache', () => this.clearCache());
|
|
132
|
+
|
|
133
|
+
console.log('[StepModuleEnhanced] Initialized v2.0.0', {
|
|
134
|
+
serverURL: this.state.serverURL,
|
|
135
|
+
cacheEnabled: this.state.cacheEnabled,
|
|
136
|
+
useBrepKernel: this.state.useBrepKernel,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// ========== CACHE INITIALIZATION ==========
|
|
141
|
+
initCache() {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const request = indexedDB.open('cycleCAD-STEP', 1);
|
|
144
|
+
request.onerror = () => {
|
|
145
|
+
console.warn('[StepModuleEnhanced] Cache disabled: IndexedDB unavailable');
|
|
146
|
+
this.state.cacheEnabled = false;
|
|
147
|
+
resolve();
|
|
148
|
+
};
|
|
149
|
+
request.onsuccess = (e) => {
|
|
150
|
+
this.db = e.target.result;
|
|
151
|
+
// Create object store if needed
|
|
152
|
+
if (!this.db.objectStoreNames.contains('glb-cache')) {
|
|
153
|
+
const store = this.db.createObjectStore('glb-cache', { keyPath: 'cacheKey' });
|
|
154
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
155
|
+
}
|
|
156
|
+
console.log('[StepModuleEnhanced] Cache initialized');
|
|
157
|
+
resolve();
|
|
158
|
+
};
|
|
159
|
+
request.onupgradeneeded = (e) => {
|
|
160
|
+
const db = e.target.result;
|
|
161
|
+
if (!db.objectStoreNames.contains('glb-cache')) {
|
|
162
|
+
db.createObjectStore('glb-cache', { keyPath: 'cacheKey' });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// ========== CACHE OPERATIONS ==========
|
|
169
|
+
async getCacheKey(file) {
|
|
170
|
+
// Simple hash of filename + size + first 1MB
|
|
171
|
+
const chunk = file.slice(0, 1024 * 1024);
|
|
172
|
+
const buffer = await chunk.arrayBuffer();
|
|
173
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
|
174
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
175
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
176
|
+
return `${file.name}-${file.size}-${hashHex.slice(0, 8)}`;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
async loadFromCache(cacheKey) {
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
if (!this.db) return resolve(null);
|
|
182
|
+
const tx = this.db.transaction('glb-cache', 'readonly');
|
|
183
|
+
const store = tx.objectStore('glb-cache');
|
|
184
|
+
const request = store.get(cacheKey);
|
|
185
|
+
request.onsuccess = () => {
|
|
186
|
+
resolve(request.result?.glbBuffer || null);
|
|
187
|
+
};
|
|
188
|
+
request.onerror = () => resolve(null);
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async saveToCache(cacheKey, glbBuffer, metadata) {
|
|
193
|
+
return new Promise((resolve) => {
|
|
194
|
+
if (!this.db) return resolve();
|
|
195
|
+
const tx = this.db.transaction('glb-cache', 'readwrite');
|
|
196
|
+
const store = tx.objectStore('glb-cache');
|
|
197
|
+
store.put({
|
|
198
|
+
cacheKey,
|
|
199
|
+
glbBuffer,
|
|
200
|
+
metadata,
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
});
|
|
203
|
+
tx.oncomplete = () => resolve();
|
|
204
|
+
tx.onerror = () => console.warn('[StepModuleEnhanced] Cache save failed');
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
async clearCache() {
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
if (!this.db) return resolve();
|
|
211
|
+
const tx = this.db.transaction('glb-cache', 'readwrite');
|
|
212
|
+
const store = tx.objectStore('glb-cache');
|
|
213
|
+
store.clear();
|
|
214
|
+
tx.oncomplete = () => {
|
|
215
|
+
console.log('[StepModuleEnhanced] Cache cleared');
|
|
216
|
+
resolve();
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// ========== SERVER HEALTH CHECK ==========
|
|
222
|
+
async checkServerHealth() {
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch(`${this.state.serverURL}/../health`, {
|
|
225
|
+
signal: AbortSignal.timeout(3000),
|
|
226
|
+
});
|
|
227
|
+
this.state.serverHealthy = response.ok;
|
|
228
|
+
if (this.state.serverHealthy) {
|
|
229
|
+
console.log('[StepModuleEnhanced] Server converter is healthy');
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
this.state.serverHealthy = false;
|
|
233
|
+
console.log('[StepModuleEnhanced] Server converter unavailable (expected in local dev)');
|
|
234
|
+
}
|
|
235
|
+
// Check periodically every 30s
|
|
236
|
+
setInterval(() => this.checkServerHealth(), 30000);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// ========== WORKER INITIALIZATION ==========
|
|
240
|
+
initWorkers() {
|
|
241
|
+
this.initOcctWorker();
|
|
242
|
+
this.initOpenCascadeWorker();
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
initOcctWorker() {
|
|
246
|
+
const workerCode = `
|
|
247
|
+
let occtImport = null;
|
|
248
|
+
let lastHeartbeat = Date.now();
|
|
249
|
+
|
|
250
|
+
importScripts('https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/occt-import-js.umd.js');
|
|
251
|
+
|
|
252
|
+
(async () => {
|
|
253
|
+
try {
|
|
254
|
+
occtImport = await window.occtImportJs({
|
|
255
|
+
locateFile: (p) => 'https://cdn.jsdelivr.net/npm/occt-import-js@0.0.23/dist/' + p
|
|
256
|
+
});
|
|
257
|
+
postMessage({ type: 'ready' });
|
|
258
|
+
} catch (e) {
|
|
259
|
+
postMessage({ type: 'error', error: 'WASM init failed: ' + e.message });
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
|
|
263
|
+
let heartbeatInterval = null;
|
|
264
|
+
|
|
265
|
+
self.onmessage = async (e) => {
|
|
266
|
+
const { type, data } = e.data;
|
|
267
|
+
|
|
268
|
+
if (type === 'parse') {
|
|
269
|
+
try {
|
|
270
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
271
|
+
|
|
272
|
+
// Heartbeat every 5s
|
|
273
|
+
heartbeatInterval = setInterval(() => {
|
|
274
|
+
postMessage({ type: 'heartbeat' });
|
|
275
|
+
}, 5000);
|
|
276
|
+
|
|
277
|
+
const startTime = performance.now();
|
|
278
|
+
const result = occtImport.ReadStepFile(data.buffer, data.deflection || 0.01);
|
|
279
|
+
const parseTime = performance.now() - startTime;
|
|
280
|
+
|
|
281
|
+
if (!result || !result.meshes || result.meshes.length === 0) {
|
|
282
|
+
throw new Error('No meshes extracted (empty STEP file or parse error)');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// **CRITICAL**: Tight .slice(0) loop to copy ALL WASM data BEFORE postMessage
|
|
286
|
+
const meshes = [];
|
|
287
|
+
for (let i = 0; i < result.meshes.length; i++) {
|
|
288
|
+
const m = result.meshes[i];
|
|
289
|
+
if (!m.attributes || !m.attributes.position) {
|
|
290
|
+
console.warn('[Worker] Mesh', i, 'missing attributes');
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const posArray = m.attributes.position.array;
|
|
295
|
+
const normArray = m.attributes.normal?.array || null;
|
|
296
|
+
const colorArray = m.attributes.color?.array || null;
|
|
297
|
+
const indexArray = m.index?.array || null;
|
|
298
|
+
|
|
299
|
+
// Tight copy: convert to standalone arrays
|
|
300
|
+
meshes.push({
|
|
301
|
+
name: m.name || 'Part_' + i,
|
|
302
|
+
position: new Float32Array(posArray.slice ? posArray.slice(0) : Array.from(posArray)),
|
|
303
|
+
normal: normArray ? new Float32Array(normArray.slice ? normArray.slice(0) : Array.from(normArray)) : null,
|
|
304
|
+
color: colorArray ? new Uint8Array(colorArray.slice ? colorArray.slice(0) : Array.from(colorArray)) : null,
|
|
305
|
+
index: indexArray ? new Uint32Array(indexArray.slice ? indexArray.slice(0) : Array.from(indexArray)) : null,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
clearInterval(heartbeatInterval);
|
|
310
|
+
|
|
311
|
+
// Transfer buffers for performance
|
|
312
|
+
const transfers = meshes
|
|
313
|
+
.map(m => [m.position.buffer, m.normal?.buffer, m.color?.buffer, m.index?.buffer])
|
|
314
|
+
.flat()
|
|
315
|
+
.filter(b => b);
|
|
316
|
+
|
|
317
|
+
postMessage({
|
|
318
|
+
type: 'complete',
|
|
319
|
+
data: { meshes, parseTime, partCount: meshes.length }
|
|
320
|
+
}, transfers);
|
|
321
|
+
|
|
322
|
+
} catch (e) {
|
|
323
|
+
clearInterval(heartbeatInterval);
|
|
324
|
+
postMessage({ type: 'error', error: e.message });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
331
|
+
const workerURL = URL.createObjectURL(blob);
|
|
332
|
+
this.worker = new Worker(workerURL);
|
|
333
|
+
|
|
334
|
+
this.worker.onmessage = (e) => {
|
|
335
|
+
const { type } = e.data;
|
|
336
|
+
if (type === 'ready') {
|
|
337
|
+
this.state.workerReady = true;
|
|
338
|
+
console.log('[StepModuleEnhanced] occt-import-js Worker ready');
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
initOpenCascadeWorker() {
|
|
344
|
+
const workerCode = `
|
|
345
|
+
let oc = null;
|
|
346
|
+
let heartbeatInterval = null;
|
|
347
|
+
|
|
348
|
+
// Load OpenCascade.js from CDN
|
|
349
|
+
importScripts('https://cdn.jsdelivr.net/npm/opencascade.js@2.0.0-beta.b5ff984/dist/opencascade.full.js');
|
|
350
|
+
|
|
351
|
+
(async () => {
|
|
352
|
+
try {
|
|
353
|
+
// Factory function is 'Module' (Emscripten pattern)
|
|
354
|
+
oc = await new Module({
|
|
355
|
+
locateFile: (file) => 'https://cdn.jsdelivr.net/npm/opencascade.js@2.0.0-beta.b5ff984/dist/' + file
|
|
356
|
+
});
|
|
357
|
+
postMessage({ type: 'ready' });
|
|
358
|
+
} catch (e) {
|
|
359
|
+
postMessage({ type: 'error', error: 'OpenCascade.js init failed: ' + e.message });
|
|
360
|
+
}
|
|
361
|
+
})();
|
|
362
|
+
|
|
363
|
+
self.onmessage = async (e) => {
|
|
364
|
+
const { type, data } = e.data;
|
|
365
|
+
|
|
366
|
+
if (type === 'parse') {
|
|
367
|
+
try {
|
|
368
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
369
|
+
heartbeatInterval = setInterval(() => {
|
|
370
|
+
postMessage({ type: 'heartbeat' });
|
|
371
|
+
}, 5000);
|
|
372
|
+
|
|
373
|
+
const startTime = performance.now();
|
|
374
|
+
|
|
375
|
+
// Use OpenCascade.js STEPControl reader
|
|
376
|
+
const reader = new oc.STEPControl_Reader();
|
|
377
|
+
const result = reader.ReadFile(new Uint8Array(data.buffer), data.buffer.byteLength);
|
|
378
|
+
|
|
379
|
+
if (result !== oc.IFSelect_RetDone) {
|
|
380
|
+
throw new Error('OpenCascade ReadFile returned status: ' + result);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
reader.TransferRoots();
|
|
384
|
+
const shape = reader.OneShape();
|
|
385
|
+
|
|
386
|
+
// Convert shape to meshes (simplified: would need proper triangulation)
|
|
387
|
+
const meshes = [];
|
|
388
|
+
// Note: Real implementation would use BRepMesh or similar
|
|
389
|
+
// For now, return empty placeholder
|
|
390
|
+
|
|
391
|
+
const parseTime = performance.now() - startTime;
|
|
392
|
+
clearInterval(heartbeatInterval);
|
|
393
|
+
|
|
394
|
+
postMessage({
|
|
395
|
+
type: 'complete',
|
|
396
|
+
data: { meshes, parseTime, partCount: meshes.length }
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
} catch (e) {
|
|
400
|
+
clearInterval(heartbeatInterval);
|
|
401
|
+
postMessage({ type: 'error', error: 'OpenCascade parse failed: ' + e.message });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
`;
|
|
406
|
+
|
|
407
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
408
|
+
const workerURL = URL.createObjectURL(blob);
|
|
409
|
+
this.opencascadeWorker = new Worker(workerURL);
|
|
410
|
+
|
|
411
|
+
this.opencascadeWorker.onmessage = (e) => {
|
|
412
|
+
const { type } = e.data;
|
|
413
|
+
if (type === 'ready') {
|
|
414
|
+
this.state.opencascadeReady = true;
|
|
415
|
+
console.log('[StepModuleEnhanced] OpenCascade.js Worker ready');
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
// ========== MAIN IMPORT ==========
|
|
421
|
+
async import(file) {
|
|
422
|
+
if (!(file instanceof File) && !(file instanceof Blob)) {
|
|
423
|
+
this.emit('step:importError', {
|
|
424
|
+
error: 'Invalid file type',
|
|
425
|
+
suggestion: 'Pass a File or Blob object'
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const filename = file.name || 'model.step';
|
|
431
|
+
const fileSize = file.size;
|
|
432
|
+
this.state.importInProgress = true;
|
|
433
|
+
this.state.importCanceled = false;
|
|
434
|
+
this.importAbortController = new AbortController();
|
|
435
|
+
|
|
436
|
+
this.emit('step:importStart', { filename, size: fileSize });
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// Check file size warnings
|
|
440
|
+
if (fileSize > 100 * 1024 * 1024) { // 100MB
|
|
441
|
+
const msg = `Large file (${(fileSize / 1024 / 1024).toFixed(1)}MB). Recommend: server converter or assembly split.`;
|
|
442
|
+
console.warn('[StepModuleEnhanced]', msg);
|
|
443
|
+
this.emit('step:importWarning', { message: msg });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Try cache first
|
|
447
|
+
const cacheKey = await this.getCacheKey(file);
|
|
448
|
+
const cachedGLB = await this.loadFromCache(cacheKey);
|
|
449
|
+
if (cachedGLB && this.state.cacheEnabled) {
|
|
450
|
+
console.log('[StepModuleEnhanced] Loaded from cache');
|
|
451
|
+
this.emit('step:importProgress', { percent: 95, message: 'Loading from cache...' });
|
|
452
|
+
await this.loadGLB(cachedGLB, filename);
|
|
453
|
+
this.state.importInProgress = false;
|
|
454
|
+
this.emit('step:importComplete', { source: 'cache' });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Route based on file size
|
|
459
|
+
let meshes;
|
|
460
|
+
if (fileSize < 30 * 1024 * 1024 && this.state.workerReady) {
|
|
461
|
+
// Route 1: occt-import-js (small files)
|
|
462
|
+
console.log('[StepModuleEnhanced] Router: small file → occt-import-js Worker');
|
|
463
|
+
meshes = await this.importViaOcctWorker(file);
|
|
464
|
+
} else if (fileSize < 50 * 1024 * 1024 && this.state.opencascadeReady) {
|
|
465
|
+
// Route 2: OpenCascade.js (medium files)
|
|
466
|
+
console.log('[StepModuleEnhanced] Router: medium file → OpenCascade.js Worker');
|
|
467
|
+
meshes = await this.importViaOpenCascadeWorker(file);
|
|
468
|
+
} else if (this.state.serverHealthy) {
|
|
469
|
+
// Route 3: Server (large files or worker unavailable)
|
|
470
|
+
console.log('[StepModuleEnhanced] Router: large file → Server converter');
|
|
471
|
+
meshes = await this.importViaServer(file);
|
|
472
|
+
} else {
|
|
473
|
+
// Fallback: try WASM if server unavailable
|
|
474
|
+
console.log('[StepModuleEnhanced] Router: fallback → occt-import-js Worker (server unavailable)');
|
|
475
|
+
if (!this.state.workerReady) {
|
|
476
|
+
throw new Error('No parser available: Workers not ready and server unavailable');
|
|
477
|
+
}
|
|
478
|
+
meshes = await this.importViaOcctWorker(file);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Create geometry and add to scene
|
|
482
|
+
const partCount = await this.createMeshesInScene(meshes, filename);
|
|
483
|
+
this.state.lastImportInfo = { partCount, filename, timestamp: Date.now() };
|
|
484
|
+
this.state.importInProgress = false;
|
|
485
|
+
|
|
486
|
+
this.emit('step:importComplete', { partCount, source: 'parsed' });
|
|
487
|
+
|
|
488
|
+
} catch (e) {
|
|
489
|
+
if (this.state.importCanceled) {
|
|
490
|
+
this.emit('step:importCanceled');
|
|
491
|
+
} else {
|
|
492
|
+
this.state.importInProgress = false;
|
|
493
|
+
console.error('[StepModuleEnhanced] Import failed:', e);
|
|
494
|
+
|
|
495
|
+
let suggestion = 'Check file format. Try smaller file or split assembly.';
|
|
496
|
+
if (e.message.includes('timeout')) {
|
|
497
|
+
suggestion = 'Parser timeout. Use server converter: localhost:8787';
|
|
498
|
+
} else if (e.message.includes('memory')) {
|
|
499
|
+
suggestion = 'Out of memory. Split assembly into sub-assemblies.';
|
|
500
|
+
} else if (e.message.includes('Server')) {
|
|
501
|
+
suggestion = 'Server unavailable. Try browser WASM or start converter service.';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this.emit('step:importError', { error: e.message, suggestion });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
// ========== IMPORT VIA OCCT WORKER ==========
|
|
510
|
+
async importViaOcctWorker(file) {
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
const reader = new FileReader();
|
|
513
|
+
const deflection = this.selectDeflection(file.size);
|
|
514
|
+
|
|
515
|
+
reader.onload = async (e) => {
|
|
516
|
+
const buffer = e.target.result;
|
|
517
|
+
const timeoutMs = 90000;
|
|
518
|
+
let timeoutHandle;
|
|
519
|
+
|
|
520
|
+
const cleanup = () => {
|
|
521
|
+
clearTimeout(timeoutHandle);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
timeoutHandle = setTimeout(() => {
|
|
525
|
+
this.worker.terminate();
|
|
526
|
+
this.initWorkers(); // Restart
|
|
527
|
+
reject(new Error('WASM timeout (90s). File too complex.'));
|
|
528
|
+
}, timeoutMs);
|
|
529
|
+
|
|
530
|
+
this.worker.onmessage = (e) => {
|
|
531
|
+
const { type, data, error } = e.data;
|
|
532
|
+
|
|
533
|
+
if (type === 'complete') {
|
|
534
|
+
cleanup();
|
|
535
|
+
resolve(data.meshes);
|
|
536
|
+
} else if (type === 'error') {
|
|
537
|
+
cleanup();
|
|
538
|
+
reject(new Error(error));
|
|
539
|
+
} else if (type === 'heartbeat') {
|
|
540
|
+
// Worker alive, reset timeout
|
|
541
|
+
clearTimeout(timeoutHandle);
|
|
542
|
+
timeoutHandle = setTimeout(() => {
|
|
543
|
+
this.worker.terminate();
|
|
544
|
+
this.initWorkers();
|
|
545
|
+
reject(new Error('WASM timeout (90s)'));
|
|
546
|
+
}, timeoutMs);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
this.emit('step:importProgress', { percent: 10, message: 'Parsing STEP (WASM)...' });
|
|
551
|
+
this.worker.postMessage({ type: 'parse', data: { buffer, deflection } });
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
reader.onerror = () => reject(new Error('File read failed'));
|
|
555
|
+
reader.readAsArrayBuffer(file);
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
// ========== IMPORT VIA OPENCASCADE WORKER ==========
|
|
560
|
+
async importViaOpenCascadeWorker(file) {
|
|
561
|
+
return new Promise((resolve, reject) => {
|
|
562
|
+
const reader = new FileReader();
|
|
563
|
+
const deflection = this.selectDeflection(file.size);
|
|
564
|
+
|
|
565
|
+
reader.onload = async (e) => {
|
|
566
|
+
const buffer = e.target.result;
|
|
567
|
+
const timeoutMs = 120000; // 2 min for larger files
|
|
568
|
+
|
|
569
|
+
let timeoutHandle = setTimeout(() => {
|
|
570
|
+
this.opencascadeWorker.terminate();
|
|
571
|
+
this.initOpenCascadeWorker();
|
|
572
|
+
reject(new Error('OpenCascade timeout (120s)'));
|
|
573
|
+
}, timeoutMs);
|
|
574
|
+
|
|
575
|
+
this.opencascadeWorker.onmessage = (e) => {
|
|
576
|
+
const { type, data, error } = e.data;
|
|
577
|
+
|
|
578
|
+
if (type === 'complete') {
|
|
579
|
+
clearTimeout(timeoutHandle);
|
|
580
|
+
resolve(data.meshes);
|
|
581
|
+
} else if (type === 'error') {
|
|
582
|
+
clearTimeout(timeoutHandle);
|
|
583
|
+
reject(new Error(error));
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
this.emit('step:importProgress', { percent: 15, message: 'Parsing STEP (OpenCascade)...' });
|
|
588
|
+
this.opencascadeWorker.postMessage({ type: 'parse', data: { buffer, deflection } });
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
reader.onerror = () => reject(new Error('File read failed'));
|
|
592
|
+
reader.readAsArrayBuffer(file);
|
|
593
|
+
});
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
// ========== IMPORT VIA SERVER ==========
|
|
597
|
+
async importViaServer(file) {
|
|
598
|
+
const formData = new FormData();
|
|
599
|
+
formData.append('file', file);
|
|
600
|
+
|
|
601
|
+
// Add adaptive deflection hint
|
|
602
|
+
const deflection = this.selectDeflection(file.size);
|
|
603
|
+
formData.append('deflection', deflection.toString());
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
this.emit('step:importProgress', { percent: 20, message: 'Uploading to server...' });
|
|
607
|
+
|
|
608
|
+
const response = await fetch(this.state.serverURL, {
|
|
609
|
+
method: 'POST',
|
|
610
|
+
body: formData,
|
|
611
|
+
signal: this.importAbortController.signal,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
if (!response.ok) {
|
|
615
|
+
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
this.emit('step:importProgress', { percent: 60, message: 'Converting on server...' });
|
|
619
|
+
|
|
620
|
+
const glbBuffer = await response.arrayBuffer();
|
|
621
|
+
|
|
622
|
+
// Cache the GLB
|
|
623
|
+
const cacheKey = await this.getCacheKey(file);
|
|
624
|
+
await this.saveToCache(cacheKey, glbBuffer, {
|
|
625
|
+
fileName: file.name,
|
|
626
|
+
fileSize: file.size,
|
|
627
|
+
deflection,
|
|
628
|
+
timestamp: Date.now(),
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
this.emit('step:importProgress', { percent: 80, message: 'Loading geometry...' });
|
|
632
|
+
|
|
633
|
+
// Parse GLB
|
|
634
|
+
return this.extractMeshesFromGLB(glbBuffer);
|
|
635
|
+
} catch (e) {
|
|
636
|
+
throw new Error(`Server import failed: ${e.message}`);
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
// ========== LOAD GLB ==========
|
|
641
|
+
async loadGLB(glbBuffer, filename) {
|
|
642
|
+
const meshes = this.extractMeshesFromGLB(glbBuffer);
|
|
643
|
+
await this.createMeshesInScene(meshes, filename);
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
extractMeshesFromGLB(glbBuffer) {
|
|
647
|
+
const loader = new THREE.GLTFLoader();
|
|
648
|
+
return new Promise((resolve, reject) => {
|
|
649
|
+
loader.parse(glbBuffer, '', (gltf) => {
|
|
650
|
+
const meshes = [];
|
|
651
|
+
gltf.scene.traverse((node) => {
|
|
652
|
+
if (node.isMesh && node.geometry) {
|
|
653
|
+
const pos = node.geometry.attributes.position;
|
|
654
|
+
const norm = node.geometry.attributes.normal;
|
|
655
|
+
const indices = node.geometry.index;
|
|
656
|
+
|
|
657
|
+
meshes.push({
|
|
658
|
+
name: node.name || 'Part',
|
|
659
|
+
position: pos.array,
|
|
660
|
+
normal: norm ? norm.array : null,
|
|
661
|
+
color: node.geometry.attributes.color?.array || null,
|
|
662
|
+
index: indices ? indices.array : null,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
resolve(meshes);
|
|
667
|
+
}, reject);
|
|
668
|
+
});
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
// ========== CREATE MESHES IN SCENE ==========
|
|
672
|
+
async createMeshesInScene(meshes, filename) {
|
|
673
|
+
let partCount = 0;
|
|
674
|
+
|
|
675
|
+
for (const meshData of meshes) {
|
|
676
|
+
if (this.state.importCanceled) break;
|
|
677
|
+
|
|
678
|
+
const geometry = new THREE.BufferGeometry();
|
|
679
|
+
|
|
680
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(meshData.position), 3));
|
|
681
|
+
if (meshData.normal) {
|
|
682
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(meshData.normal), 3));
|
|
683
|
+
} else {
|
|
684
|
+
geometry.computeVertexNormals();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (meshData.index) {
|
|
688
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(meshData.index), 1));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const material = new THREE.MeshStandardMaterial({
|
|
692
|
+
color: 0xcccccc,
|
|
693
|
+
metalness: 0.3,
|
|
694
|
+
roughness: 0.7,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
698
|
+
mesh.name = meshData.name;
|
|
699
|
+
mesh.userData = {
|
|
700
|
+
partIndex: partCount,
|
|
701
|
+
source: 'step-import',
|
|
702
|
+
filename,
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
this.kernel.exec('viewport.addMesh', { mesh, name: meshData.name });
|
|
706
|
+
|
|
707
|
+
partCount++;
|
|
708
|
+
const percent = Math.round((partCount / meshes.length) * 100);
|
|
709
|
+
this.emit('step:importProgress', { percent, message: `Creating geometry (${partCount}/${meshes.length})...` });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return partCount;
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
// ========== IMPORT FROM URL ==========
|
|
716
|
+
async importFromURL(url) {
|
|
717
|
+
try {
|
|
718
|
+
this.emit('step:importStart', { filename: url.split('/').pop(), size: 0 });
|
|
719
|
+
const response = await fetch(url, { signal: this.importAbortController.signal });
|
|
720
|
+
const blob = await response.blob();
|
|
721
|
+
const file = new File([blob], url.split('/').pop(), { type: 'application/octet-stream' });
|
|
722
|
+
return this.import(file);
|
|
723
|
+
} catch (e) {
|
|
724
|
+
this.emit('step:importError', { error: e.message });
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
// ========== EXPORT ==========
|
|
729
|
+
async export(filename = 'model.step') {
|
|
730
|
+
if (!this.state.useBrepKernel) {
|
|
731
|
+
this.emit('step:exportError', { error: 'B-Rep kernel not available' });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const shapes = this.kernel.exec('viewport.getAllShapes');
|
|
737
|
+
if (!shapes || shapes.length === 0) {
|
|
738
|
+
throw new Error('No shapes in scene to export');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const brepModule = this.kernel.modules.find(m => m.id === 'brep-kernel');
|
|
742
|
+
const stepBuffer = brepModule.exec('exportSTEP', { shapes });
|
|
743
|
+
|
|
744
|
+
const blob = new Blob([stepBuffer], { type: 'application/octet-stream' });
|
|
745
|
+
const url = URL.createObjectURL(blob);
|
|
746
|
+
const a = document.createElement('a');
|
|
747
|
+
a.href = url;
|
|
748
|
+
a.download = filename;
|
|
749
|
+
document.body.appendChild(a);
|
|
750
|
+
a.click();
|
|
751
|
+
document.body.removeChild(a);
|
|
752
|
+
URL.revokeObjectURL(url);
|
|
753
|
+
|
|
754
|
+
this.emit('step:exportComplete', { filename });
|
|
755
|
+
} catch (e) {
|
|
756
|
+
this.emit('step:exportError', { error: e.message });
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
// ========== METADATA ==========
|
|
761
|
+
async getMetadata(file) {
|
|
762
|
+
try {
|
|
763
|
+
if (this.state.serverHealthy) {
|
|
764
|
+
const formData = new FormData();
|
|
765
|
+
formData.append('file', file);
|
|
766
|
+
const response = await fetch(`${this.state.serverURL}/../metadata`, {
|
|
767
|
+
method: 'POST',
|
|
768
|
+
body: formData,
|
|
769
|
+
});
|
|
770
|
+
if (response.ok) {
|
|
771
|
+
return await response.json();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Fallback: count PART entities in ASCII STEP header
|
|
776
|
+
const reader = new FileReader();
|
|
777
|
+
return new Promise((resolve) => {
|
|
778
|
+
reader.onload = () => {
|
|
779
|
+
const text = new TextDecoder().decode(reader.result.slice(0, 100000));
|
|
780
|
+
const partCount = (text.match(/^PART\(/gm) || []).length;
|
|
781
|
+
resolve({ partCount, filename: file.name });
|
|
782
|
+
};
|
|
783
|
+
reader.readAsArrayBuffer(file);
|
|
784
|
+
});
|
|
785
|
+
} catch (e) {
|
|
786
|
+
console.error('[StepModuleEnhanced] getMetadata failed:', e);
|
|
787
|
+
return { partCount: 0, filename: file.name };
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
// ========== HELPERS ==========
|
|
792
|
+
selectDeflection(fileSize) {
|
|
793
|
+
const sizeGB = fileSize / (1024 * 1024 * 1024);
|
|
794
|
+
if (sizeGB < 0.03) return this.state.deflectionDefaults.small;
|
|
795
|
+
if (sizeGB < 0.05) return this.state.deflectionDefaults.medium;
|
|
796
|
+
if (sizeGB < 0.1) return this.state.deflectionDefaults.large;
|
|
797
|
+
return this.state.deflectionDefaults.xlarge;
|
|
798
|
+
},
|
|
799
|
+
|
|
800
|
+
setServerURL(url) {
|
|
801
|
+
this.state.serverURL = url;
|
|
802
|
+
localStorage.setItem('ev_converter_url', url);
|
|
803
|
+
this.checkServerHealth();
|
|
804
|
+
console.log('[StepModuleEnhanced] Server URL updated:', url);
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
cancelImport() {
|
|
808
|
+
this.state.importCanceled = true;
|
|
809
|
+
if (this.importAbortController) {
|
|
810
|
+
this.importAbortController.abort();
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
|
|
814
|
+
// ========== UI ==========
|
|
815
|
+
getUI() {
|
|
816
|
+
const container = document.createElement('div');
|
|
817
|
+
container.id = 'step-panel-enhanced';
|
|
818
|
+
container.style.cssText = `
|
|
819
|
+
padding: 12px;
|
|
820
|
+
background: var(--bg-secondary);
|
|
821
|
+
border-radius: 6px;
|
|
822
|
+
color: var(--text-primary);
|
|
823
|
+
font-size: 12px;
|
|
824
|
+
`;
|
|
825
|
+
|
|
826
|
+
const serverStatus = this.state.serverHealthy ? '✓ Ready' : '✗ Unavailable';
|
|
827
|
+
const serverColor = this.state.serverHealthy ? '#10b981' : '#ef4444';
|
|
828
|
+
|
|
829
|
+
container.innerHTML = `
|
|
830
|
+
<div style="margin-bottom: 12px;">
|
|
831
|
+
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 600;">STEP Import/Export</h3>
|
|
832
|
+
<p style="margin: 0; color: var(--text-secondary); font-size: 11px;">AP203/AP214 STEP files</p>
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
|
|
836
|
+
<button id="step-import-btn-enhanced" style="flex: 1; padding: 8px; background: var(--accent-blue); border: none; border-radius: 4px; color: #fff; cursor: pointer; font-size: 11px; font-weight: 500;">
|
|
837
|
+
Import STEP
|
|
838
|
+
</button>
|
|
839
|
+
<button id="step-export-btn-enhanced" style="flex: 1; padding: 8px; background: var(--accent-green); border: none; border-radius: 4px; color: #fff; cursor: pointer; font-size: 11px; font-weight: 500;" ${!this.state.useBrepKernel ? 'disabled' : ''}>
|
|
840
|
+
Export STEP
|
|
841
|
+
</button>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<div id="step-progress-enhanced" style="display: none; margin-bottom: 12px;">
|
|
845
|
+
<div style="height: 3px; background: var(--bg-tertiary); border-radius: 2px; overflow: hidden; margin-bottom: 6px;">
|
|
846
|
+
<div id="step-progress-bar-enhanced" style="height: 100%; background: var(--accent-blue); width: 0%; transition: width 0.2s;"></div>
|
|
847
|
+
</div>
|
|
848
|
+
<div style="display: flex; justify-content: space-between; font-size: 10px; color: var(--text-secondary);">
|
|
849
|
+
<span id="step-progress-text-enhanced">Importing...</span>
|
|
850
|
+
<span id="step-progress-pct-enhanced">0%</span>
|
|
851
|
+
</div>
|
|
852
|
+
<button id="step-cancel-btn" style="width: 100%; padding: 4px; margin-top: 6px; background: var(--accent-red); border: none; border-radius: 3px; color: #fff; cursor: pointer; font-size: 10px;">
|
|
853
|
+
Cancel
|
|
854
|
+
</button>
|
|
855
|
+
</div>
|
|
856
|
+
|
|
857
|
+
<div style="margin-bottom: 8px; padding: 8px; background: var(--bg-tertiary); border-radius: 4px; font-size: 10px; color: var(--text-secondary);">
|
|
858
|
+
<div style="margin-bottom: 4px;">
|
|
859
|
+
<strong>Server:</strong> <span style="color: ${serverColor}; font-weight: 500;">${serverStatus}</span>
|
|
860
|
+
</div>
|
|
861
|
+
<div style="margin-bottom: 4px;">
|
|
862
|
+
<strong>Cache:</strong> ${this.state.cacheEnabled ? 'Enabled' : 'Disabled'}
|
|
863
|
+
</div>
|
|
864
|
+
<div>
|
|
865
|
+
<strong>Routing:</strong> <30MB WASM | 30-50MB OCC | ≥50MB Server
|
|
866
|
+
</div>
|
|
867
|
+
</div>
|
|
868
|
+
|
|
869
|
+
<div style="margin-bottom: 8px;">
|
|
870
|
+
<label style="display: block; font-size: 10px; color: var(--text-secondary); margin-bottom: 4px;">Server URL</label>
|
|
871
|
+
<input id="step-server-url-enhanced" type="text" value="${this.state.serverURL}" style="width: 100%; padding: 4px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 3px; color: var(--text-primary); font-size: 10px; box-sizing: border-box;">
|
|
872
|
+
</div>
|
|
873
|
+
|
|
874
|
+
<div style="display: flex; gap: 6px; font-size: 10px;">
|
|
875
|
+
<button id="step-clear-cache-btn" style="flex: 1; padding: 4px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 3px; color: var(--text-secondary); cursor: pointer;">
|
|
876
|
+
Clear Cache
|
|
877
|
+
</button>
|
|
878
|
+
</div>
|
|
879
|
+
`;
|
|
880
|
+
|
|
881
|
+
// Event listeners
|
|
882
|
+
container.addEventListener('click', (e) => {
|
|
883
|
+
if (e.target.id === 'step-import-btn-enhanced') {
|
|
884
|
+
const input = document.createElement('input');
|
|
885
|
+
input.type = 'file';
|
|
886
|
+
input.accept = '.step,.stp';
|
|
887
|
+
input.onchange = (ev) => {
|
|
888
|
+
if (ev.target.files[0]) {
|
|
889
|
+
this.import(ev.target.files[0]);
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
input.click();
|
|
893
|
+
} else if (e.target.id === 'step-export-btn-enhanced') {
|
|
894
|
+
this.export('model.step');
|
|
895
|
+
} else if (e.target.id === 'step-cancel-btn') {
|
|
896
|
+
this.cancelImport();
|
|
897
|
+
} else if (e.target.id === 'step-clear-cache-btn') {
|
|
898
|
+
this.clearCache();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
container.addEventListener('change', (e) => {
|
|
903
|
+
if (e.target.id === 'step-server-url-enhanced') {
|
|
904
|
+
this.setServerURL(e.target.value);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// Listen to import events
|
|
909
|
+
this.kernel.on('step:importStart', () => {
|
|
910
|
+
container.querySelector('#step-progress-enhanced').style.display = 'block';
|
|
911
|
+
container.querySelector('#step-progress-bar-enhanced').style.width = '0%';
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
this.kernel.on('step:importProgress', (data) => {
|
|
915
|
+
container.querySelector('#step-progress-bar-enhanced').style.width = (data.percent || 0) + '%';
|
|
916
|
+
container.querySelector('#step-progress-text-enhanced').textContent = data.message || 'Importing...';
|
|
917
|
+
container.querySelector('#step-progress-pct-enhanced').textContent = (data.percent || 0) + '%';
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
this.kernel.on('step:importComplete', () => {
|
|
921
|
+
setTimeout(() => {
|
|
922
|
+
container.querySelector('#step-progress-enhanced').style.display = 'none';
|
|
923
|
+
}, 800);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
return container;
|
|
927
|
+
},
|
|
928
|
+
|
|
929
|
+
// ========== EVENT EMISSION ==========
|
|
930
|
+
emit(eventName, data) {
|
|
931
|
+
if (this.kernel && this.kernel.emit) {
|
|
932
|
+
this.kernel.emit(eventName, data);
|
|
933
|
+
}
|
|
934
|
+
console.log(`[StepModuleEnhanced] ${eventName}`, data);
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
export default StepModuleEnhanced;
|