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.
@@ -72,15 +72,34 @@ export class AssemblyComponentFeature {
72
72
  }
73
73
 
74
74
  async run(partHistory) {
75
+ try { console.log('[AssemblyComponentFeature] run: begin', { componentName: this.inputParams?.componentName || null, featureID: this.inputParams?.featureID || null }); } catch { }
75
76
  const componentData = await this._resolveComponentData();
76
77
  if (!componentData || !componentData.bytes || componentData.bytes.length === 0) {
77
78
  const hasSelectionIntent = Boolean((this.inputParams && this.inputParams.componentName) || (this.persistentData && this.persistentData.componentData));
78
79
  if (hasSelectionIntent) {
79
80
  console.warn('[AssemblyComponentFeature] Component payload missing or failed to load.');
80
81
  }
82
+ try { console.warn('[AssemblyComponentFeature] run: abort (no component data)'); } catch { }
81
83
  return { added: [], removed: [] };
82
84
  }
83
85
 
86
+ try {
87
+ const stats = await this._debugCount3MFTriangles(componentData.bytes);
88
+ if (stats) {
89
+ console.log('[AssemblyComponentFeature] run: 3MF model stats', stats);
90
+ }
91
+ } catch { }
92
+
93
+ try {
94
+ console.log('[AssemblyComponentFeature] run: component data', {
95
+ name: componentData.name || '',
96
+ savedAt: componentData.savedAt || null,
97
+ bytes: componentData.bytes?.length || 0,
98
+ hasFeatureInfo: !!componentData.featureInfo,
99
+ hasBrepExtras: !!componentData.featureInfo?.brepExtras,
100
+ });
101
+ } catch { }
102
+
84
103
  const featureId = this._sanitizeFeatureId(this.inputParams?.featureID);
85
104
  const group = await this._loadThreeMF(componentData.bytes);
86
105
  if (!group) {
@@ -91,6 +110,7 @@ export class AssemblyComponentFeature {
91
110
  const solids = await this._buildSolidsFromGroup(group, componentData);
92
111
  if (!solids.length) {
93
112
  console.warn('[AssemblyComponentFeature] No solids recovered from component.');
113
+ try { console.warn('[AssemblyComponentFeature] run: no solids recovered'); } catch { }
94
114
  return { added: [], removed: [] };
95
115
  }
96
116
 
@@ -150,6 +170,7 @@ export class AssemblyComponentFeature {
150
170
  featureInfo: componentData.featureInfo || null,
151
171
  };
152
172
 
173
+ try { console.log('[AssemblyComponentFeature] run: complete', { componentName, solids: solids.length }); } catch { }
153
174
  return { added: [component], removed: [] };
154
175
  }
155
176
 
@@ -158,12 +179,21 @@ export class AssemblyComponentFeature {
158
179
  if (persisted && persisted.data3mf) {
159
180
  const bytes = base64ToUint8Array(persisted.data3mf);
160
181
  let featureInfo = persisted.featureInfo;
161
- if (!featureInfo || !featureInfo.history) {
182
+ if (!featureInfo || !featureInfo.history || !featureInfo.brepExtras) {
162
183
  featureInfo = await this._extractFeatureInfo(bytes);
163
184
  }
164
185
  if (featureInfo) {
165
186
  this.persistentData.componentData.featureInfo = featureInfo;
166
187
  }
188
+ try {
189
+ console.log('[AssemblyComponentFeature] _resolveComponentData: using persisted', {
190
+ name: persisted.name || '',
191
+ savedAt: persisted.savedAt || null,
192
+ bytes: bytes?.length || 0,
193
+ hasFeatureInfo: !!featureInfo,
194
+ hasBrepExtras: !!featureInfo?.brepExtras,
195
+ });
196
+ } catch { }
167
197
  return {
168
198
  name: persisted.name || '',
169
199
  savedAt: persisted.savedAt || null,
@@ -190,6 +220,16 @@ export class AssemblyComponentFeature {
190
220
  featureInfo,
191
221
  };
192
222
 
223
+ try {
224
+ console.log('[AssemblyComponentFeature] _resolveComponentData: loaded from library', {
225
+ name: record.name || selectedName,
226
+ savedAt: record.savedAt || null,
227
+ bytes: bytes?.length || 0,
228
+ hasFeatureInfo: !!featureInfo,
229
+ hasBrepExtras: !!featureInfo?.brepExtras,
230
+ });
231
+ } catch { }
232
+
193
233
  return {
194
234
  name: record.name || selectedName,
195
235
  savedAt: record.savedAt || null,
@@ -211,15 +251,44 @@ export class AssemblyComponentFeature {
211
251
  }
212
252
  }
213
253
 
254
+ async _debugCount3MFTriangles(bytes) {
255
+ try {
256
+ const buffer = this._toArrayBuffer(bytes);
257
+ if (!buffer) return null;
258
+ const zip = await JSZip.loadAsync(buffer);
259
+ const files = {};
260
+ Object.keys(zip.files || {}).forEach(p => { files[p.toLowerCase()] = p; });
261
+ const modelPath = files['3d/3dmodel.model'] || files['/3d/3dmodel.model'];
262
+ const modelFile = modelPath ? zip.file(modelPath) : null;
263
+ if (!modelFile) return { error: 'model-file-missing' };
264
+ const xml = await modelFile.async('string');
265
+ const triCount = (xml.match(/<triangle\b/gi) || []).length;
266
+ const objCount = (xml.match(/<object\b/gi) || []).length;
267
+ return { objects: objCount, triangles: triCount };
268
+ } catch (err) {
269
+ return { error: err?.message || String(err || 'unknown') };
270
+ }
271
+ }
272
+
214
273
  async _buildSolidsFromGroup(group, componentData) {
215
274
  const solids = [];
216
275
  const componentName = this.inputParams.componentName || componentData.name || 'Component';
217
276
  const facetInfo = componentData.featureInfo?.facets || null;
218
277
  const metadataMap = componentData.featureInfo?.metadata || null;
278
+ const brepExtras = componentData?.featureInfo?.brepExtras || null;
279
+
280
+ try {
281
+ console.log('[AssemblyComponentFeature] _buildSolidsFromGroup: start', {
282
+ componentName,
283
+ hasFacetInfo: !!facetInfo,
284
+ hasMetadataMap: !!metadataMap,
285
+ hasBrepExtras: !!brepExtras,
286
+ brepExtrasSolids: brepExtras?.solids ? Object.keys(brepExtras.solids).length : 0,
287
+ });
288
+ } catch { }
219
289
 
220
290
  group.updateMatrixWorld(true);
221
291
 
222
- const faceNameCounts = new Map();
223
292
  const meshes = [];
224
293
  group.traverse((obj) => {
225
294
  const geom = obj && obj.isMesh ? obj.geometry : null;
@@ -250,7 +319,9 @@ export class AssemblyComponentFeature {
250
319
  const groupName = entry.sourceName || '';
251
320
  const groupMeshes = entry.meshes;
252
321
  const solidName = this._resolveSolidName(groupName, componentName, ++index);
253
- const built = this._buildSolidFromMeshes(groupMeshes, faceNameCounts, groupName);
322
+ const extrasForSolid = brepExtras && brepExtras.solids ? brepExtras.solids[solidName] : null;
323
+ const faceNameCounts = new Map();
324
+ const built = this._buildSolidFromMeshes(groupMeshes, faceNameCounts, groupName, extrasForSolid);
254
325
  const solid = built?.solid || null;
255
326
  const colorHints = built?.colorHints || null;
256
327
  if (!solid || !solid._triVerts || solid._triVerts.length === 0) {
@@ -262,6 +333,19 @@ export class AssemblyComponentFeature {
262
333
 
263
334
  solid.name = solidName;
264
335
 
336
+ if (extrasForSolid) {
337
+ try {
338
+ console.log('[AssemblyComponentFeature] _buildSolidsFromGroup: applying brepExtras', {
339
+ solidName,
340
+ triCount: (solid._triVerts?.length || 0) / 3,
341
+ hasTriFaceIds: !!extrasForSolid.triFaceIdsB64 || (Array.isArray(extrasForSolid.triFaceIds) && extrasForSolid.triFaceIds.length > 0),
342
+ triFaceOrder: extrasForSolid.triFaceOrder || null,
343
+ idToFaceCount: extrasForSolid.idToFaceName ? Object.keys(extrasForSolid.idToFaceName).length : 0,
344
+ });
345
+ } catch { }
346
+ this._applyBrepExtrasToSolid(solid, extrasForSolid);
347
+ }
348
+
265
349
  if (colorHints) {
266
350
  this._applyColorHintsToSolid(solid, colorHints);
267
351
  }
@@ -341,6 +425,7 @@ export class AssemblyComponentFeature {
341
425
  }
342
426
  }
343
427
 
428
+ try { console.log('[AssemblyComponentFeature] _buildSolidsFromGroup: complete', { componentName, solids: solids.length }); } catch { }
344
429
  return solids;
345
430
  }
346
431
 
@@ -364,6 +449,116 @@ export class AssemblyComponentFeature {
364
449
  try { return `#${color.getHexString()}`; } catch { return null; }
365
450
  }
366
451
 
452
+ _decodeTriFaceIds(extras) {
453
+ if (!extras) return null;
454
+ if (Array.isArray(extras.triFaceIds) && extras.triFaceIds.length) return extras.triFaceIds;
455
+ if (extras.triFaceIdsB64 && typeof extras.triFaceIdsB64 === 'string') {
456
+ try {
457
+ const bytes = base64ToUint8Array(extras.triFaceIdsB64);
458
+ if (!bytes || bytes.length < 4) return null;
459
+ const view = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
460
+ return view;
461
+ } catch {
462
+ return null;
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+
468
+ _applyBrepExtrasToSolid(solid, extras) {
469
+ if (!solid || !extras) return;
470
+
471
+ if (extras.solidMetadata && typeof extras.solidMetadata === 'object') {
472
+ solid.userData = solid.userData || {};
473
+ const existing = solid.userData.metadata && typeof solid.userData.metadata === 'object'
474
+ ? solid.userData.metadata
475
+ : {};
476
+ solid.userData.metadata = { ...existing, ...extras.solidMetadata };
477
+ }
478
+
479
+ if (extras.faceMetadata && typeof extras.faceMetadata === 'object') {
480
+ for (const [faceName, meta] of Object.entries(extras.faceMetadata)) {
481
+ if (!faceName || !meta || typeof meta !== 'object') continue;
482
+ try { solid.setFaceMetadata(faceName, meta); } catch { /* ignore */ }
483
+ }
484
+ }
485
+
486
+ if (extras.edgeMetadata && typeof extras.edgeMetadata === 'object') {
487
+ const merged = new Map(solid._edgeMetadata instanceof Map ? solid._edgeMetadata : []);
488
+ for (const [edgeName, meta] of Object.entries(extras.edgeMetadata)) {
489
+ if (!edgeName || !meta || typeof meta !== 'object') continue;
490
+ merged.set(edgeName, { ...(merged.get(edgeName) || {}), ...meta });
491
+ }
492
+ solid._edgeMetadata = merged;
493
+ }
494
+
495
+ if (Array.isArray(extras.auxEdges) && extras.auxEdges.length) {
496
+ const cleaned = [];
497
+ for (const aux of extras.auxEdges) {
498
+ const pts = Array.isArray(aux?.points)
499
+ ? aux.points.filter((p) => Array.isArray(p) && p.length === 3)
500
+ : [];
501
+ if (pts.length < 2) continue;
502
+ cleaned.push({
503
+ name: aux?.name || 'EDGE',
504
+ points: pts.map((p) => [p[0], p[1], p[2]]),
505
+ closedLoop: !!aux?.closedLoop,
506
+ polylineWorld: !!aux?.polylineWorld,
507
+ materialKey: aux?.materialKey || undefined,
508
+ centerline: !!aux?.centerline,
509
+ faceA: typeof aux?.faceA === 'string' ? aux.faceA : undefined,
510
+ faceB: typeof aux?.faceB === 'string' ? aux.faceB : undefined,
511
+ });
512
+ }
513
+ solid._auxEdges = cleaned;
514
+ }
515
+
516
+ // If we have per-triangle face IDs ordered to match 3MF material grouping,
517
+ // reapply them to guarantee face names survive loader reordering.
518
+ try {
519
+ const faceIds = this._decodeTriFaceIds(extras);
520
+ const idToFace = extras.idToFaceName && typeof extras.idToFaceName === 'object'
521
+ ? extras.idToFaceName
522
+ : null;
523
+ const triCount = (Array.isArray(solid._triVerts) ? solid._triVerts.length : 0) / 3;
524
+ const ordered = extras.triFaceOrder === 'material';
525
+ try {
526
+ console.log('[AssemblyComponentFeature] _applyBrepExtrasToSolid: tri mapping', {
527
+ solidName: solid.name || '',
528
+ triCount,
529
+ faceIdsCount: faceIds?.length || 0,
530
+ ordered,
531
+ idToFaceCount: idToFace ? Object.keys(idToFace).length : 0,
532
+ });
533
+ } catch { }
534
+ if (ordered && idToFace && faceIds && faceIds.length === triCount && triCount > 0) {
535
+ solid._triIDs = Array.from(faceIds);
536
+ const idToName = new Map();
537
+ const nameToId = new Map();
538
+ for (const [rawId, rawName] of Object.entries(idToFace)) {
539
+ const idNum = Number(rawId);
540
+ if (!Number.isFinite(idNum)) continue;
541
+ const name = String(rawName || '');
542
+ if (!name) continue;
543
+ idToName.set(idNum, name);
544
+ nameToId.set(name, idNum);
545
+ }
546
+ if (idToName.size) {
547
+ solid._idToFaceName = idToName;
548
+ solid._faceNameToID = nameToId;
549
+ }
550
+ solid._dirty = true;
551
+ solid._faceIndex = null;
552
+ try {
553
+ console.log('[AssemblyComponentFeature] _applyBrepExtrasToSolid: applied tri IDs', {
554
+ solidName: solid.name || '',
555
+ idToFaceCount: idToName.size,
556
+ });
557
+ } catch { }
558
+ }
559
+ } catch { /* ignore reapply failures */ }
560
+ }
561
+
367
562
  _hasColorEntry(metadata, keys) {
368
563
  if (!metadata || typeof metadata !== 'object') return false;
369
564
  for (const key of keys) {
@@ -376,7 +571,7 @@ export class AssemblyComponentFeature {
376
571
  return false;
377
572
  }
378
573
 
379
- _appendMeshToSolid(solid, mesh, faceNameCounts) {
574
+ _appendMeshToSolid(solid, mesh, faceNameCounts, options = {}) {
380
575
  const geometry = mesh?.geometry;
381
576
  const posAttr = geometry?.getAttribute?.('position');
382
577
  if (!solid || !geometry || !posAttr || posAttr.count < 3) return null;
@@ -388,14 +583,31 @@ export class AssemblyComponentFeature {
388
583
  const indexAttr = typeof geometry.getIndex === 'function' ? geometry.getIndex() : null;
389
584
  const materialName = this._getMaterialName(mesh.material);
390
585
  const baseFaceName = this._safeName(materialName || mesh.name || `FACE_${faceNameCounts.size + 1}`);
391
- const faceName = this._uniqueName(faceNameCounts, baseFaceName);
586
+ const preferExact = Array.isArray(options.triFaceIds) || options.triFaceIds instanceof Uint32Array;
587
+ const idToFaceName = options.idToFaceName || null;
392
588
 
393
589
  const a = new THREE.Vector3();
394
590
  const b = new THREE.Vector3();
395
591
  const c = new THREE.Vector3();
396
592
 
397
- let added = 0;
398
- const writeTriangle = (ia, ib, ic) => {
593
+ const materialArray = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
594
+ const triCount = indexAttr && indexAttr.count >= 3
595
+ ? Math.floor(indexAttr.count / 3)
596
+ : Math.floor(posAttr.count / 3);
597
+ const materialIndexByTri = new Array(triCount).fill(0);
598
+ if (Array.isArray(geometry.groups) && geometry.groups.length) {
599
+ for (const group of geometry.groups) {
600
+ const start = Math.max(0, group?.start || 0);
601
+ const count = Math.max(0, group?.count || 0);
602
+ const matIdx = Number.isFinite(group?.materialIndex) ? group.materialIndex : 0;
603
+ const triStart = Math.floor(start / 3);
604
+ const triEnd = Math.floor((start + count) / 3);
605
+ for (let t = triStart; t < triEnd && t < triCount; t++) materialIndexByTri[t] = matIdx;
606
+ }
607
+ }
608
+
609
+ const faceInfosByName = new Map();
610
+ const writeTriangle = (ia, ib, ic, faceName) => {
399
611
  if (!Number.isFinite(ia) || !Number.isFinite(ib) || !Number.isFinite(ic)) return;
400
612
  if (ia < 0 || ib < 0 || ic < 0) return;
401
613
  if (ia >= posAttr.count || ib >= posAttr.count || ic >= posAttr.count) return;
@@ -403,38 +615,85 @@ export class AssemblyComponentFeature {
403
615
  b.fromBufferAttribute(posAttr, ib).applyMatrix4(matrixWorld);
404
616
  c.fromBufferAttribute(posAttr, ic).applyMatrix4(matrixWorld);
405
617
  solid.addTriangle(faceName, [a.x, a.y, a.z], [b.x, b.y, b.z], [c.x, c.y, c.z]);
406
- added++;
407
618
  };
408
619
 
409
- if (indexAttr && indexAttr.count >= 3) {
410
- const triCount = Math.floor(indexAttr.count / 3);
411
- for (let i = 0; i < triCount; i++) {
412
- const base = i * 3;
413
- writeTriangle(indexAttr.getX(base + 0), indexAttr.getX(base + 1), indexAttr.getX(base + 2));
620
+ const fallbackFaceNames = new Map();
621
+ const resolveFallbackFaceName = (triIndex) => {
622
+ const matIdx = materialIndexByTri[triIndex] || 0;
623
+ if (!fallbackFaceNames.has(matIdx)) {
624
+ const mat = materialArray && materialArray.length ? materialArray[matIdx] : null;
625
+ const matName = this._getMaterialName(mat);
626
+ const base = this._safeName(matName || baseFaceName);
627
+ const name = faceNameCounts ? this._uniqueName(faceNameCounts, base) : base;
628
+ fallbackFaceNames.set(matIdx, name);
629
+ }
630
+ return fallbackFaceNames.get(matIdx);
631
+ };
632
+
633
+ const ids = options.triFaceIds;
634
+ const triOffset = options.triOffset || 0;
635
+ const hasExact = !!(preferExact && idToFaceName && ids && (triOffset + triCount) <= ids.length);
636
+ const resolveFaceName = (triIndex) => {
637
+ if (hasExact) {
638
+ const idx = triIndex + triOffset;
639
+ const fid = ids[idx];
640
+ const raw = idToFaceName[fid] ?? idToFaceName[String(fid)] ?? null;
641
+ if (raw) return String(raw);
642
+ return `FACE_${fid}`;
414
643
  }
415
- } else {
416
- const triCount = Math.floor(posAttr.count / 3);
417
- for (let i = 0; i < triCount; i++) {
644
+ return resolveFallbackFaceName(triIndex);
645
+ };
646
+
647
+ for (let i = 0; i < triCount; i++) {
648
+ const faceName = resolveFaceName(i);
649
+ if (!faceName) continue;
650
+ if (indexAttr && indexAttr.count >= 3) {
418
651
  const base = i * 3;
419
- writeTriangle(base + 0, base + 1, base + 2);
652
+ writeTriangle(indexAttr.getX(base + 0), indexAttr.getX(base + 1), indexAttr.getX(base + 2), faceName);
653
+ } else {
654
+ const base = i * 3;
655
+ writeTriangle(base + 0, base + 1, base + 2, faceName);
656
+ }
657
+ if (!faceInfosByName.has(faceName)) {
658
+ const mat = materialArray && materialArray.length ? materialArray[materialIndexByTri[i] || 0] : null;
659
+ faceInfosByName.set(faceName, {
660
+ faceName,
661
+ materialName: this._getMaterialName(mat) || materialName,
662
+ colorHex: this._getMaterialColorHex(mat || mesh.material),
663
+ });
420
664
  }
421
665
  }
422
666
 
423
- if (added === 0) return null;
667
+ if (solid._triVerts.length === 0) return null;
424
668
  return {
425
- faceName,
426
- materialName,
427
- colorHex: this._getMaterialColorHex(mesh.material),
669
+ faceInfos: Array.from(faceInfosByName.values()),
670
+ triCount,
428
671
  };
429
672
  }
430
673
 
431
- _buildSolidFromMeshes(meshes, faceNameCounts, sourceName) {
674
+ _buildSolidFromMeshes(meshes, faceNameCounts, sourceName, extras = null) {
432
675
  if (!Array.isArray(meshes) || meshes.length === 0) return null;
433
676
  const solid = new BREP.Solid();
434
677
  const faceInfos = [];
678
+ const triFaceIds = this._decodeTriFaceIds(extras);
679
+ const idToFaceName = (extras && extras.idToFaceName && typeof extras.idToFaceName === 'object')
680
+ ? extras.idToFaceName
681
+ : null;
682
+ let triOffset = 0;
435
683
  for (const mesh of meshes) {
436
- const info = this._appendMeshToSolid(solid, mesh, faceNameCounts);
437
- if (info) faceInfos.push(info);
684
+ const info = this._appendMeshToSolid(solid, mesh, faceNameCounts, {
685
+ triFaceIds,
686
+ triOffset,
687
+ idToFaceName,
688
+ });
689
+ if (info && Array.isArray(info.faceInfos)) {
690
+ faceInfos.push(...info.faceInfos);
691
+ } else if (info) {
692
+ faceInfos.push(info);
693
+ }
694
+ if (info && Number.isFinite(info.triCount)) {
695
+ triOffset += info.triCount;
696
+ }
438
697
  }
439
698
  if (!solid._triVerts || solid._triVerts.length === 0) return null;
440
699
  const colorHints = this._deriveColorHints(faceInfos, sourceName);
@@ -445,11 +704,18 @@ export class AssemblyComponentFeature {
445
704
  const faceColors = new Map();
446
705
  let solidColor = null;
447
706
  const solidMaterialName = sourceName ? `${sourceName}_SOLID` : '';
707
+ const defaultMaterialName = sourceName ? `${sourceName}_DEFAULT` : '';
708
+ const isDefaultMaterial = (name) => {
709
+ if (!name || typeof name !== 'string') return false;
710
+ if (defaultMaterialName && name === defaultMaterialName) return true;
711
+ return name.endsWith('_DEFAULT');
712
+ };
448
713
 
449
714
  if (Array.isArray(faceInfos)) {
450
715
  for (const info of faceInfos) {
451
716
  const hex = info?.colorHex;
452
717
  if (!hex) continue;
718
+ if (isDefaultMaterial(info.materialName)) continue;
453
719
  if (solidMaterialName && info.materialName === solidMaterialName && !solidColor) {
454
720
  solidColor = hex;
455
721
  continue;
@@ -700,6 +966,23 @@ export class AssemblyComponentFeature {
700
966
  solid._faceMetadata = renamedMetadata;
701
967
  }
702
968
 
969
+ const edgeMetadata = solid._edgeMetadata instanceof Map ? solid._edgeMetadata : null;
970
+ if (edgeMetadata && edgeMetadata.size) {
971
+ const renamedEdges = new Map();
972
+ for (const [edgeName, metadata] of edgeMetadata.entries()) {
973
+ if (!edgeName || typeof edgeName !== 'string') continue;
974
+ let renamed = edgeName;
975
+ if (edgeName.includes('|')) {
976
+ const parts = edgeName.split('|').map((p) => this._withFeaturePrefix(id, p, p));
977
+ renamed = parts.join('|');
978
+ } else {
979
+ renamed = this._withFeaturePrefix(id, edgeName, edgeName);
980
+ }
981
+ renamedEdges.set(renamed, metadata);
982
+ }
983
+ solid._edgeMetadata = renamedEdges;
984
+ }
985
+
703
986
  if (Array.isArray(solid._auxEdges) && solid._auxEdges.length) {
704
987
  for (const aux of solid._auxEdges) {
705
988
  if (!aux) continue;
@@ -748,6 +1031,18 @@ export class AssemblyComponentFeature {
748
1031
  if (!buffer) return null;
749
1032
  const zip = await JSZip.loadAsync(buffer);
750
1033
  const candidates = ['Metadata/featureHistory.json', 'metadata/featurehistory.json'];
1034
+ const extrasCandidates = ['Metadata/brepExtras.json', 'metadata/brepextras.json'];
1035
+ let brepExtras = null;
1036
+ for (const path of extrasCandidates) {
1037
+ const file = zip.file(path);
1038
+ if (!file) continue;
1039
+ try {
1040
+ const text = await file.async('string');
1041
+ const parsed = JSON.parse(text);
1042
+ if (parsed && typeof parsed === 'object') brepExtras = parsed;
1043
+ } catch { /* ignore extras parse errors */ }
1044
+ if (brepExtras) break;
1045
+ }
751
1046
  for (const path of candidates) {
752
1047
  const file = zip.file(path);
753
1048
  if (!file) continue;
@@ -759,11 +1054,13 @@ export class AssemblyComponentFeature {
759
1054
  facets: parsed?.facets || null,
760
1055
  history: parsed || null,
761
1056
  historyString: text,
1057
+ brepExtras,
762
1058
  };
763
1059
  } catch {
764
- return null;
1060
+ return brepExtras ? { brepExtras } : null;
765
1061
  }
766
1062
  }
1063
+ if (brepExtras) return { brepExtras };
767
1064
  } catch (err) {
768
1065
  console.warn('[AssemblyComponentFeature] Failed to extract feature history from component:', err);
769
1066
  }