brep-io-kernel 1.0.34 → 1.0.35
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/dist-kernel/brep-kernel.js +8357 -8091
- package/package.json +1 -1
- package/src/UI/fileManagerWidget.js +497 -77
- package/src/exporters/threeMF.js +170 -12
- package/src/features/assemblyComponent/AssemblyComponentFeature.js +322 -25
- package/src/githubStorage.js +101 -44
- package/src/services/componentLibrary.js +10 -4
package/package.json
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Designed to be embedded as an Accordion section (similar to expressionsManager).
|
|
4
4
|
import * as THREE from 'three';
|
|
5
5
|
import JSZip from 'jszip';
|
|
6
|
-
import { generate3MF } from '../exporters/threeMF.js';
|
|
6
|
+
import { generate3MF, computeTriangleMaterialIndices } from '../exporters/threeMF.js';
|
|
7
|
+
import { CADmaterials } from './CADmaterials.js';
|
|
7
8
|
import { localStorage as LS, STORAGE_BACKEND_EVENT } from '../idbStorage.js';
|
|
8
9
|
import {
|
|
9
10
|
listComponentRecords,
|
|
@@ -27,6 +28,9 @@ export class FileManagerWidget {
|
|
|
27
28
|
this._iconsOnly = this._loadIconsPref();
|
|
28
29
|
this._loadSeq = 0; // guards async load races
|
|
29
30
|
this._thumbCache = new Map();
|
|
31
|
+
this._pendingGithubMeta = new Map();
|
|
32
|
+
this._saveOverlay = null;
|
|
33
|
+
this._saveLogEl = null;
|
|
30
34
|
this._ensureStyles();
|
|
31
35
|
this._buildUI();
|
|
32
36
|
void this.refreshList();
|
|
@@ -115,8 +119,8 @@ export class FileManagerWidget {
|
|
|
115
119
|
}));
|
|
116
120
|
}
|
|
117
121
|
// Fetch one model record
|
|
118
|
-
async _getModel(name) {
|
|
119
|
-
return await getComponentRecord(name);
|
|
122
|
+
async _getModel(name, options) {
|
|
123
|
+
return await getComponentRecord(name, options);
|
|
120
124
|
}
|
|
121
125
|
// Persist one model record
|
|
122
126
|
async _setModel(name, dataObj) {
|
|
@@ -178,10 +182,66 @@ export class FileManagerWidget {
|
|
|
178
182
|
.fm-item:hover { background: #0f172a; border-color: #334155; }
|
|
179
183
|
.fm-item .fm-thumb { width: 60px; height: 60px; border: 1px solid #1f2937; background: #0b0e14; border-radius: 6px; }
|
|
180
184
|
.fm-item .fm-del { position: absolute; top: 4px; right: 4px; width: 22px; height: 22px; padding: 0; line-height: 1; }
|
|
185
|
+
|
|
186
|
+
/* Blocking save overlay */
|
|
187
|
+
.fm-save-overlay { position: fixed; inset: 0; background: rgba(2,6,23,0.65); display: flex; align-items: center; justify-content: center; z-index: 10050; }
|
|
188
|
+
.fm-save-panel { width: min(520px, 90vw); max-height: 80vh; background: #0b0e14; color: #e5e7eb; border: 1px solid #1f2937; border-radius: 12px; padding: 16px 18px; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
|
189
|
+
.fm-save-title { font-weight: 700; font-size: 14px; letter-spacing: .01em; margin-bottom: 10px; }
|
|
190
|
+
.fm-save-log { font-size: 12px; line-height: 1.4; max-height: 52vh; overflow: auto; white-space: pre-wrap; color: #cbd5f5; background: #0a0f1a; border: 1px solid #1f2937; border-radius: 8px; padding: 10px; }
|
|
191
|
+
.fm-save-line { margin-bottom: 6px; }
|
|
181
192
|
`;
|
|
182
193
|
document.head.appendChild(style);
|
|
183
194
|
}
|
|
184
195
|
|
|
196
|
+
_setSaveBusy(isBusy) {
|
|
197
|
+
try {
|
|
198
|
+
if (this.saveBtn) this.saveBtn.disabled = !!isBusy;
|
|
199
|
+
if (this.nameInput) this.nameInput.disabled = !!isBusy;
|
|
200
|
+
} catch { /* ignore */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_startSaveProgress(title) {
|
|
204
|
+
try {
|
|
205
|
+
this._endSaveProgress();
|
|
206
|
+
const overlay = document.createElement('div');
|
|
207
|
+
overlay.className = 'fm-save-overlay';
|
|
208
|
+
const panel = document.createElement('div');
|
|
209
|
+
panel.className = 'fm-save-panel';
|
|
210
|
+
const header = document.createElement('div');
|
|
211
|
+
header.className = 'fm-save-title';
|
|
212
|
+
header.textContent = title || 'Saving...';
|
|
213
|
+
const log = document.createElement('div');
|
|
214
|
+
log.className = 'fm-save-log';
|
|
215
|
+
panel.appendChild(header);
|
|
216
|
+
panel.appendChild(log);
|
|
217
|
+
overlay.appendChild(panel);
|
|
218
|
+
document.body.appendChild(overlay);
|
|
219
|
+
this._saveOverlay = overlay;
|
|
220
|
+
this._saveLogEl = log;
|
|
221
|
+
} catch { /* ignore */ }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
_logSaveProgress(message) {
|
|
225
|
+
try {
|
|
226
|
+
if (!this._saveLogEl) return;
|
|
227
|
+
const line = document.createElement('div');
|
|
228
|
+
line.className = 'fm-save-line';
|
|
229
|
+
line.textContent = message || '';
|
|
230
|
+
this._saveLogEl.appendChild(line);
|
|
231
|
+
this._saveLogEl.scrollTop = this._saveLogEl.scrollHeight;
|
|
232
|
+
} catch { /* ignore */ }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_endSaveProgress() {
|
|
236
|
+
try {
|
|
237
|
+
if (this._saveOverlay && this._saveOverlay.parentNode) {
|
|
238
|
+
this._saveOverlay.parentNode.removeChild(this._saveOverlay);
|
|
239
|
+
}
|
|
240
|
+
} catch { /* ignore */ }
|
|
241
|
+
this._saveOverlay = null;
|
|
242
|
+
this._saveLogEl = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
185
245
|
_buildUI() {
|
|
186
246
|
// Header: name input + Save
|
|
187
247
|
const header = document.createElement('div');
|
|
@@ -204,6 +264,7 @@ export class FileManagerWidget {
|
|
|
204
264
|
saveBtn.textContent = 'Save';
|
|
205
265
|
saveBtn.className = 'fm-btn';
|
|
206
266
|
saveBtn.addEventListener('click', () => this.saveCurrent());
|
|
267
|
+
this.saveBtn = saveBtn;
|
|
207
268
|
header.appendChild(saveBtn);
|
|
208
269
|
this.uiElement.appendChild(header);
|
|
209
270
|
|
|
@@ -228,6 +289,24 @@ export class FileManagerWidget {
|
|
|
228
289
|
this._refreshHistoryCollections('new-model');
|
|
229
290
|
}
|
|
230
291
|
|
|
292
|
+
async _retryGithubOperation(action, op, progress) {
|
|
293
|
+
while (true) {
|
|
294
|
+
try {
|
|
295
|
+
const value = await op();
|
|
296
|
+
return { ok: true, value };
|
|
297
|
+
} catch (err) {
|
|
298
|
+
const msg = (err && typeof err.message === 'string' && err.message.trim())
|
|
299
|
+
? err.message.trim()
|
|
300
|
+
: (err ? String(err) : '');
|
|
301
|
+
const details = msg ? `\n\n${msg}` : '';
|
|
302
|
+
try { if (typeof progress === 'function') progress(`${action} failed.${details}`); } catch { }
|
|
303
|
+
const retry = await window.confirm(`${action} failed.${details}\n\nRetry?`);
|
|
304
|
+
try { if (typeof progress === 'function') progress(retry ? 'Retrying...' : 'Save canceled by user.'); } catch { }
|
|
305
|
+
if (!retry) return { ok: false, error: err };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
231
310
|
async saveCurrent() {
|
|
232
311
|
if (!this.viewer || !this.viewer.partHistory) return;
|
|
233
312
|
let name = (this.nameInput.value || '').trim();
|
|
@@ -238,86 +317,203 @@ export class FileManagerWidget {
|
|
|
238
317
|
this.nameInput.value = name;
|
|
239
318
|
}
|
|
240
319
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let modelMetadata = undefined;
|
|
245
|
-
if (jsonString) {
|
|
246
|
-
additionalFiles = { 'Metadata/featureHistory.json': jsonString };
|
|
247
|
-
modelMetadata = { featureHistoryPath: '/Metadata/featureHistory.json' };
|
|
248
|
-
}
|
|
249
|
-
// Embed PMI view images under /views
|
|
320
|
+
try { console.log('[FileManagerWidget] saveCurrent: begin', { name }); } catch { }
|
|
321
|
+
this._setSaveBusy(true);
|
|
322
|
+
this._startSaveProgress(`Saving "${name}"...`);
|
|
250
323
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
324
|
+
this._logSaveProgress('Preparing feature history...');
|
|
325
|
+
// Get feature history JSON (now includes PMI views) and embed into a 3MF archive as Metadata/featureHistory.json
|
|
326
|
+
const jsonString = await this.viewer.partHistory.toJSON();
|
|
327
|
+
try { console.log('[FileManagerWidget] saveCurrent: feature history', { bytes: jsonString ? jsonString.length : 0 }); } catch { }
|
|
328
|
+
let additionalFiles = {};
|
|
329
|
+
let modelMetadata = undefined;
|
|
330
|
+
if (jsonString) {
|
|
331
|
+
additionalFiles['Metadata/featureHistory.json'] = jsonString;
|
|
332
|
+
modelMetadata = { featureHistoryPath: '/Metadata/featureHistory.json' };
|
|
254
333
|
}
|
|
255
|
-
|
|
256
|
-
console.error('Failed to embed PMI view images:', err);
|
|
257
|
-
}
|
|
258
|
-
// Capture a 60x60 thumbnail of the current view
|
|
259
|
-
let thumbnail = null;
|
|
260
|
-
try {
|
|
261
|
-
thumbnail = await this._captureThumbnail(60);
|
|
262
|
-
} catch { /* ignore thumbnail failures */ }
|
|
263
|
-
|
|
264
|
-
// Collect solids for full 3MF export (so slicers can open it).
|
|
265
|
-
const solids = this._collectSolidsForExport();
|
|
266
|
-
const solidsForExport = [];
|
|
267
|
-
const skipped = [];
|
|
268
|
-
solids.forEach((s, idx) => {
|
|
334
|
+
// Embed PMI view images under /views
|
|
269
335
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
skipped.push(s?.name || `solid_${idx}`);
|
|
336
|
+
this._logSaveProgress('Capturing PMI view images...');
|
|
337
|
+
const viewFiles = await this.viewer?.pmiViewsWidget?.captureViewImagesForPackage?.();
|
|
338
|
+
if (viewFiles && typeof viewFiles === 'object') {
|
|
339
|
+
additionalFiles = { ...(additionalFiles || {}), ...viewFiles };
|
|
275
340
|
}
|
|
276
|
-
} catch {
|
|
277
|
-
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error('Failed to embed PMI view images:', err);
|
|
278
343
|
}
|
|
279
|
-
|
|
344
|
+
// Capture a 60x60 thumbnail of the current view
|
|
345
|
+
let thumbnail = null;
|
|
346
|
+
try {
|
|
347
|
+
this._logSaveProgress('Capturing thumbnail...');
|
|
348
|
+
thumbnail = await this._captureThumbnail(60);
|
|
349
|
+
} catch { /* ignore thumbnail failures */ }
|
|
280
350
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
precision: 6,
|
|
299
|
-
scale: 1,
|
|
300
|
-
additionalFiles,
|
|
301
|
-
modelMetadata,
|
|
302
|
-
thumbnail,
|
|
303
|
-
metadataManager,
|
|
351
|
+
// Collect solids for full 3MF export (so slicers can open it).
|
|
352
|
+
this._logSaveProgress('Collecting solids...');
|
|
353
|
+
const solids = this._collectSolidsForExport();
|
|
354
|
+
try { console.log('[FileManagerWidget] saveCurrent: collected solids', { count: solids.length, names: solids.map(s => s?.name).filter(Boolean) }); } catch { }
|
|
355
|
+
const solidsForExport = [];
|
|
356
|
+
const skipped = [];
|
|
357
|
+
solids.forEach((s, idx) => {
|
|
358
|
+
try {
|
|
359
|
+
const mesh = s?.getMesh?.();
|
|
360
|
+
if (mesh && mesh.vertProperties && mesh.triVerts) {
|
|
361
|
+
solidsForExport.push(s);
|
|
362
|
+
} else {
|
|
363
|
+
skipped.push(s?.name || `solid_${idx}`);
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
skipped.push(s?.name || `solid_${idx}`);
|
|
367
|
+
}
|
|
304
368
|
});
|
|
305
|
-
console.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
369
|
+
try { console.log('[FileManagerWidget] saveCurrent: solids for export', { count: solidsForExport.length, skipped }); } catch { }
|
|
370
|
+
|
|
371
|
+
// Attach BREP-specific metadata for mesh-based restores (face names, colors, centerlines).
|
|
372
|
+
try {
|
|
373
|
+
this._logSaveProgress('Packaging BREP metadata...');
|
|
374
|
+
const extras = this._buildBrepExtras(solidsForExport);
|
|
375
|
+
try { console.log('[FileManagerWidget] saveCurrent: brepExtras', { hasExtras: !!extras, solidCount: extras?.solids ? Object.keys(extras.solids).length : 0 }); } catch { }
|
|
376
|
+
if (extras) {
|
|
377
|
+
additionalFiles = additionalFiles || {};
|
|
378
|
+
additionalFiles['Metadata/brepExtras.json'] = JSON.stringify(extras);
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.warn('[FileManagerWidget] Failed to embed BREP extras:', err);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let threeMfBytes;
|
|
385
|
+
try {
|
|
386
|
+
this._logSaveProgress('Exporting 3MF...');
|
|
387
|
+
const metadataManager = this.viewer?.partHistory?.metadataManager || null;
|
|
388
|
+
const defaultFaceColor = (() => {
|
|
389
|
+
try {
|
|
390
|
+
const color = CADmaterials?.FACE?.BASE?.color;
|
|
391
|
+
if (color && typeof color.getHexString === 'function') {
|
|
392
|
+
return `#${color.getHexString()}`;
|
|
393
|
+
}
|
|
394
|
+
if (typeof color === 'string') return color;
|
|
395
|
+
} catch { }
|
|
396
|
+
return null;
|
|
397
|
+
})();
|
|
398
|
+
threeMfBytes = await generate3MF(solidsForExport, {
|
|
399
|
+
unit: 'millimeter',
|
|
400
|
+
precision: 6,
|
|
401
|
+
scale: 1,
|
|
402
|
+
additionalFiles,
|
|
403
|
+
modelMetadata,
|
|
404
|
+
thumbnail,
|
|
405
|
+
metadataManager,
|
|
406
|
+
defaultFaceColor,
|
|
407
|
+
});
|
|
408
|
+
try { console.log('[FileManagerWidget] saveCurrent: 3MF exported', { bytes: threeMfBytes?.length || 0 }); } catch { }
|
|
409
|
+
try {
|
|
410
|
+
const zip = await JSZip.loadAsync(threeMfBytes);
|
|
411
|
+
const files = {};
|
|
412
|
+
Object.keys(zip.files || {}).forEach(p => { files[p.toLowerCase()] = p; });
|
|
413
|
+
const modelPath = files['3d/3dmodel.model'] || files['/3d/3dmodel.model'];
|
|
414
|
+
const modelFile = modelPath ? zip.file(modelPath) : null;
|
|
415
|
+
if (modelFile) {
|
|
416
|
+
const xml = await modelFile.async('string');
|
|
417
|
+
const triCount = (xml.match(/<triangle\b/gi) || []).length;
|
|
418
|
+
const objCount = (xml.match(/<object\b/gi) || []).length;
|
|
419
|
+
console.log('[FileManagerWidget] saveCurrent: 3MF model stats', { objects: objCount, triangles: triCount });
|
|
420
|
+
} else {
|
|
421
|
+
console.warn('[FileManagerWidget] saveCurrent: 3MF model file not found in zip');
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
try { console.warn('[FileManagerWidget] saveCurrent: 3MF model stats failed', err?.message || err); } catch { }
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
// Fallback: history only 3MF
|
|
428
|
+
const metadataManager = this.viewer?.partHistory?.metadataManager || null;
|
|
429
|
+
const defaultFaceColor = (() => {
|
|
430
|
+
try {
|
|
431
|
+
const color = CADmaterials?.FACE?.BASE?.color;
|
|
432
|
+
if (color && typeof color.getHexString === 'function') {
|
|
433
|
+
return `#${color.getHexString()}`;
|
|
434
|
+
}
|
|
435
|
+
if (typeof color === 'string') return color;
|
|
436
|
+
} catch { }
|
|
437
|
+
return null;
|
|
438
|
+
})();
|
|
439
|
+
threeMfBytes = await generate3MF([], {
|
|
440
|
+
unit: 'millimeter',
|
|
441
|
+
precision: 6,
|
|
442
|
+
scale: 1,
|
|
443
|
+
additionalFiles,
|
|
444
|
+
modelMetadata,
|
|
445
|
+
thumbnail,
|
|
446
|
+
metadataManager,
|
|
447
|
+
defaultFaceColor,
|
|
448
|
+
});
|
|
449
|
+
console.warn('[FileManagerWidget] 3MF export failed for solids, saved history-only 3MF.', e);
|
|
450
|
+
try { console.log('[FileManagerWidget] saveCurrent: 3MF exported (history only)', { bytes: threeMfBytes?.length || 0 }); } catch { }
|
|
451
|
+
try {
|
|
452
|
+
const zip = await JSZip.loadAsync(threeMfBytes);
|
|
453
|
+
const files = {};
|
|
454
|
+
Object.keys(zip.files || {}).forEach(p => { files[p.toLowerCase()] = p; });
|
|
455
|
+
const modelPath = files['3d/3dmodel.model'] || files['/3d/3dmodel.model'];
|
|
456
|
+
const modelFile = modelPath ? zip.file(modelPath) : null;
|
|
457
|
+
if (modelFile) {
|
|
458
|
+
const xml = await modelFile.async('string');
|
|
459
|
+
const triCount = (xml.match(/<triangle\b/gi) || []).length;
|
|
460
|
+
const objCount = (xml.match(/<object\b/gi) || []).length;
|
|
461
|
+
console.log('[FileManagerWidget] saveCurrent: 3MF model stats (history only)', { objects: objCount, triangles: triCount });
|
|
462
|
+
} else {
|
|
463
|
+
console.warn('[FileManagerWidget] saveCurrent: 3MF model file not found in zip (history only)');
|
|
464
|
+
}
|
|
465
|
+
} catch (err) {
|
|
466
|
+
try { console.warn('[FileManagerWidget] saveCurrent: 3MF model stats failed (history only)', err?.message || err); } catch { }
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const threeMfB64 = uint8ArrayToBase64(threeMfBytes);
|
|
470
|
+
const now = new Date().toISOString();
|
|
471
|
+
|
|
472
|
+
// Store only the 3MF (with embedded thumbnail) and timestamp
|
|
473
|
+
const record = { savedAt: now, data3mf: threeMfB64 };
|
|
474
|
+
if (thumbnail) record.thumbnail = thumbnail;
|
|
475
|
+
if (LS?.isGithub?.()) {
|
|
476
|
+
this._logSaveProgress('Saving to GitHub...');
|
|
477
|
+
try { console.log('[FileManagerWidget] saveCurrent: saving to GitHub', { name }); } catch { }
|
|
478
|
+
const res = await this._retryGithubOperation(
|
|
479
|
+
`Save "${name}" to GitHub`,
|
|
480
|
+
() => this._setModel(name, record),
|
|
481
|
+
(msg) => this._logSaveProgress(msg)
|
|
482
|
+
);
|
|
483
|
+
if (!res.ok) {
|
|
484
|
+
this._logSaveProgress('Save canceled.');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
this._pendingGithubMeta.set(name, {
|
|
489
|
+
savedAt: record.savedAt || null,
|
|
490
|
+
thumbnail: record.thumbnail || null,
|
|
491
|
+
});
|
|
492
|
+
} catch { /* ignore */ }
|
|
493
|
+
} else {
|
|
494
|
+
this._logSaveProgress('Saving to local storage...');
|
|
495
|
+
try { console.log('[FileManagerWidget] saveCurrent: saving locally', { name }); } catch { }
|
|
496
|
+
await this._setModel(name, record);
|
|
497
|
+
}
|
|
498
|
+
// Update in-memory thumbnail cache so UI reflects the new preview immediately
|
|
499
|
+
try { if (thumbnail) this._thumbCache.set(name, thumbnail); } catch { }
|
|
500
|
+
this.currentName = name;
|
|
501
|
+
this._saveLastName(name);
|
|
502
|
+
this._logSaveProgress('Refreshing list...');
|
|
503
|
+
await this.refreshList();
|
|
504
|
+
this._logSaveProgress('Save complete.');
|
|
505
|
+
try { console.log('[FileManagerWidget] saveCurrent: complete', { name }); } catch { }
|
|
506
|
+
if (skipped.length) {
|
|
507
|
+
try { console.warn('[FileManagerWidget] Skipped non-manifold solids:', skipped); } catch {}
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
const msg = (err && err.message) ? err.message : String(err || 'Unknown error');
|
|
511
|
+
this._logSaveProgress(`Save failed: ${msg}`);
|
|
512
|
+
try { console.warn('[FileManagerWidget] saveCurrent: failed', { name, error: msg }); } catch { }
|
|
513
|
+
throw err;
|
|
514
|
+
} finally {
|
|
515
|
+
this._endSaveProgress();
|
|
516
|
+
this._setSaveBusy(false);
|
|
321
517
|
}
|
|
322
518
|
}
|
|
323
519
|
|
|
@@ -333,10 +529,214 @@ export class FileManagerWidget {
|
|
|
333
529
|
return selected.length ? selected : solids;
|
|
334
530
|
}
|
|
335
531
|
|
|
532
|
+
_buildBrepExtras(solids) {
|
|
533
|
+
if (!Array.isArray(solids) || solids.length === 0) return null;
|
|
534
|
+
|
|
535
|
+
const cleanMeta = (value) => {
|
|
536
|
+
if (value == null) return null;
|
|
537
|
+
try {
|
|
538
|
+
return JSON.parse(JSON.stringify(value, (key, v) => {
|
|
539
|
+
if (typeof v === 'function') return undefined;
|
|
540
|
+
if (v && v.isColor && typeof v.getHexString === 'function') {
|
|
541
|
+
try { return `#${v.getHexString()}`; } catch { return v; }
|
|
542
|
+
}
|
|
543
|
+
return v;
|
|
544
|
+
}));
|
|
545
|
+
} catch {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const mapToObject = (map) => {
|
|
551
|
+
if (!(map instanceof Map) || map.size === 0) return null;
|
|
552
|
+
const out = {};
|
|
553
|
+
for (const [key, val] of map.entries()) {
|
|
554
|
+
if (key == null) continue;
|
|
555
|
+
const cleaned = cleanMeta(val);
|
|
556
|
+
if (cleaned != null) out[String(key)] = cleaned;
|
|
557
|
+
}
|
|
558
|
+
return Object.keys(out).length ? out : null;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const encodeTriIds = (triIds) => {
|
|
562
|
+
if (!triIds || triIds.length === 0) return '';
|
|
563
|
+
const u32 = triIds instanceof Uint32Array ? triIds : Uint32Array.from(triIds);
|
|
564
|
+
const u8 = new Uint8Array(u32.buffer, u32.byteOffset, u32.byteLength);
|
|
565
|
+
return uint8ArrayToBase64(u8);
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const solidsOut = {};
|
|
569
|
+
const metadataManager = this.viewer?.partHistory?.metadataManager;
|
|
570
|
+
for (const solid of solids) {
|
|
571
|
+
if (!solid || solid.type !== 'SOLID') continue;
|
|
572
|
+
const name = String(solid.name || '').trim();
|
|
573
|
+
if (!name) continue;
|
|
574
|
+
|
|
575
|
+
const authorTriCount = Array.isArray(solid._triVerts) ? (solid._triVerts.length / 3) : 0;
|
|
576
|
+
const authorTriIdCount = Array.isArray(solid._triIDs) ? solid._triIDs.length : 0;
|
|
577
|
+
let triIds = solid._triIDs || [];
|
|
578
|
+
let triCount = (Array.isArray(triIds) || triIds instanceof Uint32Array) ? triIds.length : 0;
|
|
579
|
+
let triIdsOrdered = triIds;
|
|
580
|
+
let mesh = null;
|
|
581
|
+
let triMat = null;
|
|
582
|
+
let meshTriCount = 0;
|
|
583
|
+
let meshFaceIdCount = 0;
|
|
584
|
+
try {
|
|
585
|
+
if (typeof solid.getMesh === 'function') {
|
|
586
|
+
mesh = solid.getMesh();
|
|
587
|
+
if (mesh && mesh.faceID && mesh.faceID.length) {
|
|
588
|
+
triIds = Array.from(mesh.faceID);
|
|
589
|
+
triCount = triIds.length;
|
|
590
|
+
}
|
|
591
|
+
meshTriCount = (mesh?.triVerts && mesh.triVerts.length) ? (mesh.triVerts.length / 3) : 0;
|
|
592
|
+
meshFaceIdCount = (mesh?.faceID && mesh.faceID.length) ? mesh.faceID.length : 0;
|
|
593
|
+
try {
|
|
594
|
+
triMat = computeTriangleMaterialIndices(solid, mesh, {
|
|
595
|
+
metadataManager,
|
|
596
|
+
includeFaceTags: true,
|
|
597
|
+
useMetadataColors: true,
|
|
598
|
+
});
|
|
599
|
+
} catch { /* ignore material mapping */ }
|
|
600
|
+
}
|
|
601
|
+
} catch { /* ignore mesh failures */ }
|
|
602
|
+
finally { try { mesh?.delete?.(); } catch { } }
|
|
603
|
+
|
|
604
|
+
if (triMat && Array.isArray(triMat) && triMat.length === triCount && triCount > 0) {
|
|
605
|
+
const buckets = new Map();
|
|
606
|
+
let defaultBucket = null;
|
|
607
|
+
for (let t = 0; t < triCount; t++) {
|
|
608
|
+
const fid = triIds[t];
|
|
609
|
+
const midx = triMat[t];
|
|
610
|
+
if (midx == null || !Number.isFinite(midx)) {
|
|
611
|
+
if (!defaultBucket) defaultBucket = [];
|
|
612
|
+
defaultBucket.push(fid);
|
|
613
|
+
} else {
|
|
614
|
+
const key = Number(midx);
|
|
615
|
+
let arr = buckets.get(key);
|
|
616
|
+
if (!arr) { arr = []; buckets.set(key, arr); }
|
|
617
|
+
arr.push(fid);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (buckets.size || (defaultBucket && defaultBucket.length)) {
|
|
621
|
+
const ordered = [];
|
|
622
|
+
const keys = Array.from(buckets.keys()).sort((a, b) => a - b);
|
|
623
|
+
for (const k of keys) {
|
|
624
|
+
const arr = buckets.get(k);
|
|
625
|
+
if (arr && arr.length) ordered.push(...arr);
|
|
626
|
+
}
|
|
627
|
+
if (defaultBucket && defaultBucket.length) ordered.push(...defaultBucket);
|
|
628
|
+
triIdsOrdered = ordered;
|
|
629
|
+
triCount = triIdsOrdered.length;
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
triIdsOrdered = triIds;
|
|
633
|
+
triCount = (Array.isArray(triIdsOrdered) || triIdsOrdered instanceof Uint32Array) ? triIdsOrdered.length : 0;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
console.log('[FileManagerWidget] brepExtras: counts', {
|
|
637
|
+
name,
|
|
638
|
+
authorTriCount,
|
|
639
|
+
authorTriIdCount,
|
|
640
|
+
meshTriCount,
|
|
641
|
+
meshFaceIdCount,
|
|
642
|
+
triIdsCount: (Array.isArray(triIds) || triIds instanceof Uint32Array) ? triIds.length : 0,
|
|
643
|
+
triIdsOrderedCount: (Array.isArray(triIdsOrdered) || triIdsOrdered instanceof Uint32Array) ? triIdsOrdered.length : 0,
|
|
644
|
+
triMatCount: Array.isArray(triMat) ? triMat.length : 0,
|
|
645
|
+
});
|
|
646
|
+
} catch { }
|
|
647
|
+
try {
|
|
648
|
+
console.log('[FileManagerWidget] brepExtras: solid', {
|
|
649
|
+
name,
|
|
650
|
+
triCount,
|
|
651
|
+
faceMapCount: idToFaceName ? Object.keys(idToFaceName).length : 0,
|
|
652
|
+
faceMetaCount: faceMetadata ? Object.keys(faceMetadata).length : 0,
|
|
653
|
+
edgeMetaCount: edgeMetadata ? Object.keys(edgeMetadata).length : 0,
|
|
654
|
+
triFaceOrder: 'material',
|
|
655
|
+
});
|
|
656
|
+
} catch { }
|
|
657
|
+
let idToFaceName = (solid._idToFaceName instanceof Map)
|
|
658
|
+
? Object.fromEntries(Array.from(solid._idToFaceName.entries()).map(([k, v]) => [String(k), String(v)]))
|
|
659
|
+
: null;
|
|
660
|
+
if (!idToFaceName && solid._faceNameToID instanceof Map) {
|
|
661
|
+
const inverted = {};
|
|
662
|
+
for (const [faceName, faceId] of solid._faceNameToID.entries()) {
|
|
663
|
+
if (faceId == null || faceName == null) continue;
|
|
664
|
+
inverted[String(faceId)] = String(faceName);
|
|
665
|
+
}
|
|
666
|
+
if (Object.keys(inverted).length) idToFaceName = inverted;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let faceMetadata = mapToObject(solid._faceMetadata);
|
|
670
|
+
const edgeMetadata = mapToObject(solid._edgeMetadata);
|
|
671
|
+
const solidUserMeta = cleanMeta(solid?.userData?.metadata || null);
|
|
672
|
+
const solidManagerMeta = (metadataManager && typeof metadataManager.getMetadata === 'function')
|
|
673
|
+
? cleanMeta(metadataManager.getMetadata(name))
|
|
674
|
+
: null;
|
|
675
|
+
const solidMetadata = solidManagerMeta
|
|
676
|
+
? { ...(solidManagerMeta || {}), ...(solidUserMeta || {}) }
|
|
677
|
+
: solidUserMeta;
|
|
678
|
+
|
|
679
|
+
if (metadataManager && typeof metadataManager.getMetadata === 'function' && idToFaceName) {
|
|
680
|
+
const mergedFaceMeta = faceMetadata || {};
|
|
681
|
+
for (const faceName of Object.values(idToFaceName)) {
|
|
682
|
+
if (!faceName) continue;
|
|
683
|
+
const meta = cleanMeta(metadataManager.getMetadata(faceName));
|
|
684
|
+
if (meta && typeof meta === 'object' && Object.keys(meta).length) {
|
|
685
|
+
mergedFaceMeta[faceName] = { ...(meta || {}), ...(mergedFaceMeta[faceName] || {}) };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
faceMetadata = Object.keys(mergedFaceMeta).length ? mergedFaceMeta : faceMetadata;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let auxEdges = null;
|
|
692
|
+
if (Array.isArray(solid._auxEdges) && solid._auxEdges.length) {
|
|
693
|
+
auxEdges = solid._auxEdges.map((e) => {
|
|
694
|
+
const pts = Array.isArray(e?.points)
|
|
695
|
+
? e.points
|
|
696
|
+
.map((p) => (Array.isArray(p) && p.length === 3 ? [p[0], p[1], p[2]] : null))
|
|
697
|
+
.filter(Boolean)
|
|
698
|
+
: [];
|
|
699
|
+
return {
|
|
700
|
+
name: e?.name || '',
|
|
701
|
+
points: pts,
|
|
702
|
+
closedLoop: !!e?.closedLoop,
|
|
703
|
+
polylineWorld: !!e?.polylineWorld,
|
|
704
|
+
materialKey: e?.materialKey || undefined,
|
|
705
|
+
centerline: !!e?.centerline,
|
|
706
|
+
faceA: typeof e?.faceA === 'string' ? e.faceA : undefined,
|
|
707
|
+
faceB: typeof e?.faceB === 'string' ? e.faceB : undefined,
|
|
708
|
+
};
|
|
709
|
+
}).filter((e) => Array.isArray(e.points) && e.points.length >= 2);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (faceMetadata && Object.keys(faceMetadata).length === 0) faceMetadata = null;
|
|
713
|
+
solidsOut[name] = {
|
|
714
|
+
triCount,
|
|
715
|
+
triFaceIdsB64: encodeTriIds(triIdsOrdered),
|
|
716
|
+
triFaceOrder: 'material',
|
|
717
|
+
idToFaceName,
|
|
718
|
+
faceMetadata,
|
|
719
|
+
edgeMetadata,
|
|
720
|
+
auxEdges,
|
|
721
|
+
solidMetadata,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!Object.keys(solidsOut).length) return null;
|
|
726
|
+
return { version: 1, solids: solidsOut };
|
|
727
|
+
}
|
|
728
|
+
|
|
336
729
|
async loadModel(name) {
|
|
337
730
|
if (!this.viewer || !this.viewer.partHistory) return;
|
|
338
731
|
const seq = ++this._loadSeq; // only the last call should win
|
|
339
|
-
|
|
732
|
+
let rec = null;
|
|
733
|
+
if (LS?.isGithub?.()) {
|
|
734
|
+
const res = await this._retryGithubOperation(`Load "${name}" from GitHub`, () => this._getModel(name, { throwOnError: true }));
|
|
735
|
+
if (!res.ok) return;
|
|
736
|
+
rec = res.value;
|
|
737
|
+
} else {
|
|
738
|
+
rec = await this._getModel(name);
|
|
739
|
+
}
|
|
340
740
|
if (!rec) return alert('Model not found.');
|
|
341
741
|
await this.viewer.partHistory.reset();
|
|
342
742
|
// Prefer new 3MF-based storage
|
|
@@ -461,6 +861,26 @@ export class FileManagerWidget {
|
|
|
461
861
|
|
|
462
862
|
async refreshList() {
|
|
463
863
|
const items = await this._listModels();
|
|
864
|
+
if (LS?.isGithub?.() && this._pendingGithubMeta && this._pendingGithubMeta.size) {
|
|
865
|
+
for (const it of items) {
|
|
866
|
+
const pending = this._pendingGithubMeta.get(it.name);
|
|
867
|
+
if (!pending) continue;
|
|
868
|
+
const itemTime = it.savedAt ? Date.parse(it.savedAt) : NaN;
|
|
869
|
+
const pendingTime = pending.savedAt ? Date.parse(pending.savedAt) : NaN;
|
|
870
|
+
if (!Number.isFinite(itemTime) || (Number.isFinite(pendingTime) && pendingTime > itemTime)) {
|
|
871
|
+
if (pending.savedAt) it.savedAt = pending.savedAt;
|
|
872
|
+
if (it.record && pending.savedAt) it.record.savedAt = pending.savedAt;
|
|
873
|
+
if (pending.thumbnail) {
|
|
874
|
+
it.thumbnail = pending.thumbnail;
|
|
875
|
+
if (it.record) it.record.thumbnail = pending.thumbnail;
|
|
876
|
+
try { this._thumbCache.set(it.name, pending.thumbnail); } catch { }
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
// Remote metadata caught up; drop the pending override.
|
|
880
|
+
this._pendingGithubMeta.delete(it.name);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
464
884
|
while (this.listEl.firstChild) this.listEl.removeChild(this.listEl.firstChild);
|
|
465
885
|
|
|
466
886
|
if (!items.length) {
|