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.
@@ -211,10 +211,161 @@ function _resolveColorHex(meta, keys) {
211
211
  return _parseColorToHex(raw);
212
212
  }
213
213
 
214
+ /**
215
+ * Compute per-triangle material indices as they will be assigned in build3MFModelXML.
216
+ * Returns an array of length triCount where each entry is a material index (number),
217
+ * or null when the triangle would fall back to the default resource (no material).
218
+ *
219
+ * This mirrors the metadata color / face-tag fallback logic used by the exporter,
220
+ * so downstream importers can predict ThreeMFLoader's mesh ordering.
221
+ */
222
+ export function computeTriangleMaterialIndices(solid, mesh, opts = {}) {
223
+ try {
224
+ if (!solid || !mesh) return null;
225
+ const triVerts = mesh.triVerts;
226
+ const triCount = (triVerts && triVerts.length) ? ((triVerts.length / 3) | 0) : 0;
227
+ if (!triCount) return null;
228
+
229
+ const faceIDs = (mesh.faceID && mesh.faceID.length === triCount) ? mesh.faceID : null;
230
+ if (!faceIDs || !faceIDs.length) return null;
231
+
232
+ const useMetadataColors = opts.useMetadataColors !== false;
233
+ const includeFaceTags = opts.includeFaceTags !== false;
234
+ const metadataManager = opts.metadataManager && typeof opts.metadataManager.getMetadata === 'function'
235
+ ? opts.metadataManager
236
+ : null;
237
+
238
+ let idToFaceName = (solid && solid._idToFaceName instanceof Map) ? solid._idToFaceName : null;
239
+ if (!idToFaceName && solid && solid._faceNameToID instanceof Map) {
240
+ const inverted = new Map();
241
+ for (const [faceName, faceId] of solid._faceNameToID.entries()) {
242
+ if (faceId == null || faceName == null) continue;
243
+ inverted.set(faceId, String(faceName));
244
+ }
245
+ if (inverted.size) idToFaceName = inverted;
246
+ }
247
+
248
+ let solidColorHex = null;
249
+ if (useMetadataColors) {
250
+ try {
251
+ const solidMeta = (metadataManager && solid?.name)
252
+ ? metadataManager.getMetadata(solid.name)
253
+ : null;
254
+ solidColorHex = _resolveColorHex(solidMeta, ['solidColor', 'color'])
255
+ || _resolveColorHex(solid?.userData?.metadata || null, ['solidColor', 'color']);
256
+ } catch {
257
+ solidColorHex = null;
258
+ }
259
+ }
260
+
261
+ let faceColorById = null;
262
+ if (useMetadataColors && idToFaceName) {
263
+ faceColorById = new Map();
264
+ const seen = new Set();
265
+ for (let t = 0; t < faceIDs.length; t++) {
266
+ const fid = faceIDs[t] >>> 0;
267
+ if (seen.has(fid)) continue;
268
+ seen.add(fid);
269
+ const faceName = idToFaceName.get(fid) || `FACE_${fid}`;
270
+ let faceHex = null;
271
+ if (metadataManager) {
272
+ try { faceHex = _resolveColorHex(metadataManager.getMetadata(faceName), ['faceColor', 'color']); } catch { faceHex = null; }
273
+ }
274
+ if (!faceHex) {
275
+ try {
276
+ const faceMeta = (typeof solid.getFaceMetadata === 'function') ? solid.getFaceMetadata(faceName) : null;
277
+ faceHex = _resolveColorHex(faceMeta, ['faceColor', 'color']);
278
+ } catch { faceHex = null; }
279
+ }
280
+ if (faceHex) faceColorById.set(fid, faceHex);
281
+ }
282
+ }
283
+
284
+ const hasFaceColors = !!(faceColorById && faceColorById.size);
285
+ const hasSolidColor = !!solidColorHex;
286
+ const hasMetadataColors = !!(useMetadataColors && (hasSolidColor || hasFaceColors));
287
+
288
+ let faceMatIndexById = null;
289
+ let solidMatIndex = null;
290
+ let defaultMatIndex = null;
291
+ let useFaceTagsFallback = false;
292
+
293
+ if (hasMetadataColors) {
294
+ const colorToIndex = new Map();
295
+ const addMaterial = (hex, label) => {
296
+ if (!hex) return null;
297
+ if (!colorToIndex.has(hex)) {
298
+ colorToIndex.set(hex, colorToIndex.size);
299
+ }
300
+ return colorToIndex.get(hex);
301
+ };
302
+ if (hasSolidColor) {
303
+ const solidLabel = solid?.name ? `${solid.name}_SOLID` : 'SOLID';
304
+ solidMatIndex = addMaterial(solidColorHex, solidLabel);
305
+ } else {
306
+ const defaultLabel = solid?.name ? `${solid.name}_DEFAULT` : 'DEFAULT';
307
+ const defaultHex = _parseColorToHex(opts.defaultFaceColor) || '#c0c0c0';
308
+ defaultMatIndex = addMaterial(defaultHex, defaultLabel);
309
+ }
310
+ if (hasFaceColors && faceColorById) {
311
+ faceMatIndexById = new Map();
312
+ for (const [fid, hex] of faceColorById.entries()) {
313
+ const faceName = idToFaceName ? (idToFaceName.get(fid) || `FACE_${fid}`) : `FACE_${fid}`;
314
+ const idx = addMaterial(hex, faceName);
315
+ faceMatIndexById.set(fid, idx);
316
+ }
317
+ }
318
+ } else if (includeFaceTags) {
319
+ useFaceTagsFallback = true;
320
+ }
321
+
322
+ let faceIndexOf = null;
323
+ if (useFaceTagsFallback) {
324
+ const uniqueIds = [];
325
+ const seen = new Set();
326
+ for (let t = 0; t < faceIDs.length; t++) {
327
+ const fid = faceIDs[t] >>> 0;
328
+ if (seen.has(fid)) continue;
329
+ seen.add(fid);
330
+ uniqueIds.push(fid);
331
+ }
332
+ const idToMatIdx = new Map();
333
+ for (let i = 0; i < uniqueIds.length; i++) idToMatIdx.set(uniqueIds[i], i);
334
+ faceIndexOf = (fid) => idToMatIdx.get(fid) ?? 0;
335
+ }
336
+
337
+ if (!hasMetadataColors && !useFaceTagsFallback && solidMatIndex == null) {
338
+ return null;
339
+ }
340
+
341
+ const triMat = new Array(triCount);
342
+ for (let t = 0; t < triCount; t++) {
343
+ const fid = faceIDs[t] >>> 0;
344
+ let idx = null;
345
+ if (faceMatIndexById && faceMatIndexById.has(fid)) {
346
+ idx = faceMatIndexById.get(fid);
347
+ } else if (useFaceTagsFallback && faceIndexOf) {
348
+ idx = faceIndexOf(fid);
349
+ } else if (solidMatIndex != null) {
350
+ idx = solidMatIndex;
351
+ } else if (defaultMatIndex != null) {
352
+ idx = defaultMatIndex;
353
+ } else {
354
+ idx = null;
355
+ }
356
+ triMat[t] = idx;
357
+ }
358
+
359
+ return triMat;
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+
214
365
  /**
215
366
  * Build the core 3MF model XML for one or more solids.
216
367
  * @param {Array} solids Array of SOLID-like objects that expose getMesh() and name.
217
- * @param {{unit?: 'millimeter'|'inch'|'foot'|'meter'|'centimeter'|'micron', precision?: number, scale?: number, metadataManager?: any, useMetadataColors?: boolean, includeFaceTags?: boolean, applyWorldTransform?: boolean}} opts
368
+ * @param {{unit?: 'millimeter'|'inch'|'foot'|'meter'|'centimeter'|'micron', precision?: number, scale?: number, metadataManager?: any, useMetadataColors?: boolean, includeFaceTags?: boolean, applyWorldTransform?: boolean, defaultFaceColor?: any}} opts
218
369
  * @returns {string}
219
370
  */
220
371
  export function build3MFModelXML(solids, opts = {}) {
@@ -276,6 +427,7 @@ export function build3MFModelXML(solids, opts = {}) {
276
427
  let faceColorById = null;
277
428
  let faceMatIndexById = null;
278
429
  let solidMatIndex = null;
430
+ let defaultMatIndex = null;
279
431
  let useFaceTagsFallback = false;
280
432
 
281
433
  const faceIDs = (mesh.faceID && mesh.faceID.length === tCount) ? mesh.faceID : null;
@@ -324,15 +476,20 @@ export function build3MFModelXML(solids, opts = {}) {
324
476
  return colorToIndex.get(hex);
325
477
  };
326
478
 
327
- if (hasSolidColor) solidMatIndex = addMaterial(solidColorHex, `${rawName}_SOLID`);
328
- if (hasFaceColors) {
329
- faceMatIndexById = new Map();
330
- for (const [fid, hex] of faceColorById.entries()) {
331
- const faceName = idToFaceName ? (idToFaceName.get(fid) || `FACE_${fid}`) : `FACE_${fid}`;
332
- const idx = addMaterial(hex, faceName);
333
- faceMatIndexById.set(fid, idx);
334
- }
479
+ if (hasSolidColor) solidMatIndex = addMaterial(solidColorHex, `${rawName}_SOLID`);
480
+ if (!hasSolidColor) {
481
+ // Ensure triangles without per-face colors still map to a basematerial
482
+ const defaultHex = _parseColorToHex(opts.defaultFaceColor) || '#c0c0c0';
483
+ defaultMatIndex = addMaterial(defaultHex, `${rawName}_DEFAULT`);
484
+ }
485
+ if (hasFaceColors) {
486
+ faceMatIndexById = new Map();
487
+ for (const [fid, hex] of faceColorById.entries()) {
488
+ const faceName = idToFaceName ? (idToFaceName.get(fid) || `FACE_${fid}`) : `FACE_${fid}`;
489
+ const idx = addMaterial(hex, faceName);
490
+ faceMatIndexById.set(fid, idx);
335
491
  }
492
+ }
336
493
 
337
494
  if (materials.length > 0) {
338
495
  matPid = nextId++;
@@ -343,9 +500,10 @@ export function build3MFModelXML(solids, opts = {}) {
343
500
  lines.push(` <base${nameAttr} displaycolor="${entry.color}"/>`);
344
501
  }
345
502
  lines.push(' </basematerials>');
346
- if (solidMatIndex != null) {
503
+ if (solidMatIndex != null || defaultMatIndex != null) {
347
504
  objectPidAttr = ` pid="${matPid}"`;
348
- objectPindexAttr = ` pindex="${solidMatIndex}"`;
505
+ const idx = (solidMatIndex != null) ? solidMatIndex : defaultMatIndex;
506
+ objectPindexAttr = ` pindex="${idx}"`;
349
507
  }
350
508
  }
351
509
  } else if (includeFaceTags && faceIDs) {
@@ -514,7 +672,7 @@ function rootRelsXML({ thumbnailPath, viewImages } = {}) {
514
672
  /**
515
673
  * Generate a 3MF zip archive as Uint8Array.
516
674
  * @param {Array} solids Array of SOLID-like objects that expose getMesh() and name.
517
- * @param {{unit?: string, precision?: number, scale?: number, metadataManager?: any, useMetadataColors?: boolean, includeFaceTags?: boolean, applyWorldTransform?: boolean}} opts
675
+ * @param {{unit?: string, precision?: number, scale?: number, metadataManager?: any, useMetadataColors?: boolean, includeFaceTags?: boolean, applyWorldTransform?: boolean, defaultFaceColor?: any}} opts
518
676
  * @returns {Promise<Uint8Array>}
519
677
  */
520
678
  export async function generate3MF(solids, opts = {}) {