@niivue/niivue 0.44.0 → 0.44.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niivue/niivue",
3
- "version": "0.44.0",
3
+ "version": "0.44.2",
4
4
  "description": "minimal webgl2 nifti image viewer",
5
5
  "types": "./build/index.d.ts",
6
6
  "main": "./build/index.js",
@@ -72,31 +72,32 @@
72
72
  "rxjs": "^7.8.1"
73
73
  },
74
74
  "devDependencies": {
75
- "@playwright/test": "^1.42.1",
76
- "@rollup/plugin-commonjs": "^25.0.7",
75
+ "@playwright/test": "^1.45.2",
76
+ "@rollup/plugin-commonjs": "^26.0.1",
77
77
  "@types/array-equal": "^1.0.2",
78
- "@types/node": "^20.12.2",
78
+ "@types/node": "^20.14.11",
79
79
  "@types/ungap__structured-clone": "^1.2.0",
80
- "@typescript-eslint/eslint-plugin": "^7.5.0",
81
- "@typescript-eslint/parser": "^7.5.0",
82
- "@vitest/coverage-v8": "^1.4.0",
80
+ "@typescript-eslint/eslint-plugin": "^7.16.1",
81
+ "@typescript-eslint/parser": "^7.16.1",
82
+ "@vitest/coverage-v8": "^2.0.3",
83
83
  "eslint": "^8.57.0",
84
84
  "eslint-config-prettier": "^9.1.0",
85
85
  "eslint-config-standard": "^17.1.0",
86
- "eslint-plugin-prettier": "^5.1.3",
87
- "eslint-plugin-tsdoc": "^0.2.17",
86
+ "eslint-plugin-prettier": "^5.2.1",
87
+ "eslint-plugin-tsdoc": "^0.3.0",
88
88
  "express": "^4.19.2",
89
- "happy-dom": "^14.3.10",
89
+ "happy-dom": "^14.12.3",
90
90
  "http-server": "^14.1.1",
91
- "prettier": "^3.2.5",
91
+ "prettier": "^3.3.3",
92
92
  "regenerator-runtime": "^0.14.1",
93
- "tsup": "^8.0.2",
94
- "typedoc": "^0.25.12",
95
- "typescript": "^5.4.3",
96
- "vite": "^5.2.7",
97
- "vitest": "^1.4.0"
93
+ "terser": "^5.31.3",
94
+ "tsup": "^8.1.2",
95
+ "typedoc": "^0.26.4",
96
+ "typescript": "^5.5.3",
97
+ "vite": "^5.3.4",
98
+ "vitest": "^2.0.3"
98
99
  },
99
100
  "optionalDependencies": {
100
- "@rollup/rollup-linux-x64-gnu": "^4.13.2"
101
+ "@rollup/rollup-linux-x64-gnu": "^4.18.1"
101
102
  }
102
103
  }
@@ -41,6 +41,7 @@ import {
41
41
  fragMeshMatcapShader,
42
42
  fragMeshOutlineShader,
43
43
  fragMeshEdgeShader,
44
+ fragMeshShaderCrevice,
44
45
  fragMeshDiffuseEdgeShader,
45
46
  fragMeshHemiShader,
46
47
  fragMeshMatteShader,
@@ -123,6 +124,8 @@ export * from '../nvdocument.js'
123
124
  export { NVUtilities } from '../nvutilities.js'
124
125
  export { LabelTextAlignment, LabelLineTerminator, NVLabel3DStyle, NVLabel3D } from '../nvlabel.js'
125
126
  export { NVMeshLoaders } from '../nvmesh-loaders.js'
127
+ export { NVMeshUtilities } from '../nvmesh-utilities.js'
128
+
126
129
  // same rollup error as above during npm run dev, and during the umd build
127
130
  // TODO: at least remove the umd build when AFNI do not need it anymore
128
131
  export * from '../types.js'
@@ -477,6 +480,10 @@ export class Niivue {
477
480
  Name: 'Hemispheric',
478
481
  Frag: fragMeshHemiShader
479
482
  },
483
+ {
484
+ Name: 'Crevice',
485
+ Frag: fragMeshShaderCrevice
486
+ },
480
487
  {
481
488
  Name: 'Edge',
482
489
  Frag: fragMeshEdgeShader
@@ -2049,7 +2056,7 @@ export class Niivue {
2049
2056
  this.addVolume(volume)
2050
2057
  // set drawing enabled to make sure
2051
2058
  // the new drawing bitmap matches the background volume dims
2052
- this.setDrawingEnabled(true)
2059
+ // this.setDrawingEnabled(true)
2053
2060
  }
2054
2061
  })
2055
2062
  .catch((e) => {
@@ -2807,6 +2814,25 @@ export class Niivue {
2807
2814
  return this.meshes[idx].indexNearestXYZmm(Xmm, Ymm, Zmm)
2808
2815
  }
2809
2816
 
2817
+ /**
2818
+ * reduce complexity of FreeSurfer mesh
2819
+ * @param mesh - identity of mesh to change
2820
+ * @param order - decimation order 0..6
2821
+ * @example niivue.decimateHierarchicalMesh(niivue.meshes[0].id, 4)
2822
+ * @returns boolean false if mesh is not hierarchical or of lower order
2823
+ * @see {@link https://niivue.github.io/niivue/features/meshes.html | live demo usage}
2824
+ */
2825
+ decimateHierarchicalMesh(mesh: number, order: number = 3): boolean {
2826
+ const idx = this.getMeshIndexByID(mesh)
2827
+ if (idx < 0) {
2828
+ log.warn('reverseFaces() id not loaded', mesh)
2829
+ return
2830
+ }
2831
+ const ret = this.meshes[idx].decimateHierarchicalMesh(this.gl, order)
2832
+ this.updateGLVolume()
2833
+ return ret
2834
+ }
2835
+
2810
2836
  /**
2811
2837
  * reverse triangle winding of mesh (swap front and back faces)
2812
2838
  * @param id - identity of mesh to change
@@ -3366,6 +3392,9 @@ export class Niivue {
3366
3392
  this.renderShader!.use(this.gl)
3367
3393
  this.gl.uniform3fv(this.renderShader!.uniforms.clipLo!, this.opts.clipVolumeLow)
3368
3394
  this.gl.uniform3fv(this.renderShader!.uniforms.clipHi!, this.opts.clipVolumeHigh)
3395
+ this.pickingImageShader!.use(this.gl)
3396
+ this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipLo!, this.opts.clipVolumeLow)
3397
+ this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipHi!, this.opts.clipVolumeHigh)
3369
3398
  this.drawScene()
3370
3399
  }
3371
3400
 
@@ -5947,7 +5976,8 @@ export class Niivue {
5947
5976
  this.pickingImageShader.use(this.gl)
5948
5977
  this.gl.uniform1f(this.pickingImageShader.uniforms.overlays, this.overlays.length)
5949
5978
  this.gl.uniform3fv(this.pickingImageShader.uniforms.texVox, vox)
5950
-
5979
+ this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipLo!, this.opts.clipVolumeLow)
5980
+ this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipHi!, this.opts.clipVolumeHigh)
5951
5981
  let shader = this.sliceMMShader
5952
5982
  if (this.opts.isV1SliceShader) {
5953
5983
  shader = this.sliceV1Shader
@@ -3369,7 +3369,7 @@ export class NVImage {
3369
3369
  * read a 3D slab of voxels from a volume
3370
3370
  * @param voxStart - first row, column and slice (RAS order) for selection
3371
3371
  * @param voxEnd - final row, column and slice (RAS order) for selection
3372
- * @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32'
3372
+ * @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32', 'scaled', 'normalized', 'windowed'
3373
3373
  * @returns the an array where ret[0] is the voxel values and ret[1] is dimension of selection
3374
3374
  * @see {@link https://niivue.github.io/niivue/features/slab_selection.html | live demo usage}
3375
3375
  */
@@ -3394,7 +3394,12 @@ export class NVImage {
3394
3394
  let dt = this.hdr!.datatypeCode
3395
3395
  if (dataType === 'uint8') {
3396
3396
  dt = NiiDataType.DT_UINT8
3397
- } else if (dataType === 'float32') {
3397
+ } else if (
3398
+ dataType === 'float32' ||
3399
+ dataType === 'scaled' ||
3400
+ dataType === 'normalized' ||
3401
+ dataType === 'windowed'
3402
+ ) {
3398
3403
  dt = NiiDataType.DT_FLOAT32
3399
3404
  }
3400
3405
  if (dt === NiiDataType.DT_UINT8) {
@@ -3424,6 +3429,24 @@ export class NVImage {
3424
3429
  }
3425
3430
  }
3426
3431
  }
3432
+ if (dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {
3433
+ for (let i = 0; i < img.length; i++) {
3434
+ img[i] = img[i] * this.hdr.scl_slope + this.hdr.scl_inter
3435
+ }
3436
+ }
3437
+ if (dataType === 'normalized' || dataType === 'windowed') {
3438
+ let mn = this.cal_min
3439
+ let mx = this.cal_max
3440
+ if (dataType === 'normalized') {
3441
+ mn = this.global_min
3442
+ mx = this.global_max
3443
+ }
3444
+ const scale = 1 / (mx - mn)
3445
+ for (let i = 0; i < img.length; i++) {
3446
+ img[i] = (img[i] - mn) * scale
3447
+ img[i] = Math.max(Math.min(img[i], 1), 0)
3448
+ }
3449
+ }
3427
3450
  return [img, slabDims]
3428
3451
  } // getVolumeData()
3429
3452
 
@@ -373,10 +373,13 @@ following conditions are met:
373
373
  return m
374
374
  }
375
375
 
376
- function str2Buffer(str: string): number[] {
376
+ function str2Buffer(str: string, maxLen: number = 80): number[] {
377
377
  // emulate node.js Buffer.from
378
+ // remove characters than could be used for shell expansion
379
+ str = str.replace(/[`$]/g, '')
378
380
  const bytes = []
379
- for (let i = 0; i < str.length; i++) {
381
+ const len = Math.min(maxLen, str.length)
382
+ for (let i = 0; i < len; i++) {
380
383
  const char = str.charCodeAt(i)
381
384
  bytes.push(char & 0xff)
382
385
  }
@@ -384,70 +387,79 @@ function str2Buffer(str: string): number[] {
384
387
  }
385
388
 
386
389
  // save NIfTI header into UINT8 array for saving to disk
387
- export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false): Uint8Array {
390
+ export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false, isInputEndian = false): Uint8Array {
388
391
  const SHORT_SIZE = 2
389
392
  const FLOAT32_SIZE = 4
390
-
393
+ let isLittleEndian = true
394
+ if (isInputEndian) {
395
+ isLittleEndian = hdr.littleEndian
396
+ }
391
397
  const byteArray = new Uint8Array(348)
392
398
  const view = new DataView(byteArray.buffer)
393
399
  // sizeof_hdr
394
- view.setInt32(0, 348, hdr.littleEndian)
400
+ view.setInt32(0, 348, isLittleEndian)
395
401
 
396
402
  // data_type, db_name, extents, session_error, regular are not used
397
-
403
+ // regular set to 'r' (ASCII 114) for Analyze compatibility
404
+ view.setUint8(38, 114)
398
405
  // dim_info
399
406
  view.setUint8(39, hdr.dim_info)
400
407
 
401
408
  // dims
402
409
  for (let i = 0; i < 8; i++) {
403
- view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], hdr.littleEndian)
410
+ view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian)
404
411
  }
405
412
 
406
413
  // intent_p1, intent_p2, intent_p3
407
- view.setFloat32(56, hdr.intent_p1, hdr.littleEndian)
408
- view.setFloat32(60, hdr.intent_p2, hdr.littleEndian)
409
- view.setFloat32(64, hdr.intent_p3, hdr.littleEndian)
414
+ view.setFloat32(56, hdr.intent_p1, isLittleEndian)
415
+ view.setFloat32(60, hdr.intent_p2, isLittleEndian)
416
+ view.setFloat32(64, hdr.intent_p3, isLittleEndian)
410
417
  // intent_code, datatype, bitpix, slice_start
411
- view.setInt16(68, hdr.intent_code, hdr.littleEndian)
418
+ view.setInt16(68, hdr.intent_code, isLittleEndian)
412
419
  if (isDrawing8) {
413
- view.setInt16(70, 2, hdr.littleEndian) // 2 = DT_UINT8
414
- view.setInt16(72, 8, hdr.littleEndian)
420
+ view.setInt16(70, 2, isLittleEndian) // 2 = DT_UINT8
421
+ view.setInt16(72, 8, isLittleEndian)
415
422
  } else {
416
- view.setInt16(70, hdr.datatypeCode, hdr.littleEndian)
417
- view.setInt16(72, hdr.numBitsPerVoxel, hdr.littleEndian)
423
+ view.setInt16(70, hdr.datatypeCode, isLittleEndian)
424
+ view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian)
418
425
  }
419
- view.setInt16(74, hdr.slice_start, hdr.littleEndian)
426
+ view.setInt16(74, hdr.slice_start, isLittleEndian)
420
427
 
421
428
  // pixdim[8], vox_offset, scl_slope, scl_inter
422
429
  for (let i = 0; i < 8; i++) {
423
- view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], hdr.littleEndian)
430
+ view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian)
424
431
  }
425
432
  if (isDrawing8) {
426
- view.setFloat32(108, 352, hdr.littleEndian)
427
- view.setFloat32(112, 1.0, hdr.littleEndian)
428
- view.setFloat32(116, 0.0, hdr.littleEndian)
433
+ view.setFloat32(108, 352, isLittleEndian)
434
+ view.setFloat32(112, 1.0, isLittleEndian)
435
+ view.setFloat32(116, 0.0, isLittleEndian)
429
436
  } else {
430
- view.setFloat32(108, hdr.vox_offset, hdr.littleEndian)
431
- view.setFloat32(112, hdr.scl_slope, hdr.littleEndian)
432
- view.setFloat32(116, hdr.scl_inter, hdr.littleEndian)
437
+ // view.setFloat32(108, hdr.vox_offset, isLittleEndian)
438
+ view.setFloat32(108, 352, isLittleEndian)
439
+ view.setFloat32(112, hdr.scl_slope, isLittleEndian)
440
+ view.setFloat32(116, hdr.scl_inter, isLittleEndian)
433
441
  }
434
442
  // slice_end
435
- view.setInt16(120, hdr.slice_end, hdr.littleEndian)
443
+ view.setInt16(120, hdr.slice_end, isLittleEndian)
436
444
 
437
445
  // slice_code, xyzt_units
438
446
  view.setUint8(122, hdr.slice_code)
439
- view.setUint8(123, hdr.xyzt_units)
447
+ if (hdr.xyzt_units === 0) {
448
+ view.setUint8(123, 10)
449
+ } else {
450
+ view.setUint8(123, hdr.xyzt_units)
451
+ }
440
452
 
441
453
  // cal_max, cal_min, slice_duration, toffset
442
454
  if (isDrawing8) {
443
- view.setFloat32(124, 0, hdr.littleEndian)
444
- view.setFloat32(128, 0, hdr.littleEndian)
455
+ view.setFloat32(124, 0, isLittleEndian)
456
+ view.setFloat32(128, 0, isLittleEndian)
445
457
  } else {
446
- view.setFloat32(124, hdr.cal_max, hdr.littleEndian)
447
- view.setFloat32(128, hdr.cal_min, hdr.littleEndian)
458
+ view.setFloat32(124, hdr.cal_max, isLittleEndian)
459
+ view.setFloat32(128, hdr.cal_min, isLittleEndian)
448
460
  }
449
- view.setFloat32(132, hdr.slice_duration, hdr.littleEndian)
450
- view.setFloat32(136, hdr.toffset, hdr.littleEndian)
461
+ view.setFloat32(132, hdr.slice_duration, isLittleEndian)
462
+ view.setFloat32(136, hdr.toffset, isLittleEndian)
451
463
 
452
464
  // glmax, glmin are unused
453
465
 
@@ -457,27 +469,34 @@ export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false): Uint8Arr
457
469
  // node.js: byteArray.set(Buffer.from(hdr.aux_file), 228);
458
470
  byteArray.set(str2Buffer(hdr.aux_file), 228)
459
471
  // qform_code, sform_code
460
- view.setInt16(252, hdr.qform_code, hdr.littleEndian)
461
- view.setInt16(254, hdr.sform_code, hdr.littleEndian)
472
+ view.setInt16(252, hdr.qform_code, isLittleEndian)
473
+ // if sform unknown, assume NIFTI_XFORM_SCANNER_ANAT
474
+ if (hdr.sform_code < 1 || hdr.sform_code < 1) {
475
+ view.setInt16(254, 1, isLittleEndian)
476
+ } else {
477
+ view.setInt16(254, hdr.sform_code, isLittleEndian)
478
+ }
462
479
 
463
480
  // quatern_b, quatern_c, quatern_d, qoffset_x, qoffset_y, qoffset_z, srow_x[4], srow_y[4], and srow_z[4]
464
- view.setFloat32(256, hdr.quatern_b, hdr.littleEndian)
465
- view.setFloat32(260, hdr.quatern_c, hdr.littleEndian)
466
- view.setFloat32(264, hdr.quatern_d, hdr.littleEndian)
467
- view.setFloat32(268, hdr.qoffset_x, hdr.littleEndian)
468
- view.setFloat32(272, hdr.qoffset_y, hdr.littleEndian)
469
- view.setFloat32(276, hdr.qoffset_z, hdr.littleEndian)
481
+ view.setFloat32(256, hdr.quatern_b, isLittleEndian)
482
+ view.setFloat32(260, hdr.quatern_c, isLittleEndian)
483
+ view.setFloat32(264, hdr.quatern_d, isLittleEndian)
484
+ view.setFloat32(268, hdr.qoffset_x, isLittleEndian)
485
+ view.setFloat32(272, hdr.qoffset_y, isLittleEndian)
486
+ view.setFloat32(276, hdr.qoffset_z, isLittleEndian)
470
487
  const flattened = hdr.affine.flat()
471
488
  // we only want the first three rows
472
489
  for (let i = 0; i < 12; i++) {
473
- view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], hdr.littleEndian)
490
+ view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian)
474
491
  }
475
492
  // node.js https://www.w3schools.com/nodejs/met_buffer_from.asp
476
493
  // intent_name and magic
477
494
  // node.js byteArray.set(Buffer.from(hdr.intent_name), 328);
478
- byteArray.set(str2Buffer(hdr.intent_name), 328)
495
+ // byteArray.set(str2Buffer(hdr.intent_name), 328)
479
496
  // node.js byteArray.set(Buffer.from(hdr.magic), 344);
480
- byteArray.set(str2Buffer(hdr.magic), 344)
497
+ // byteArray.set(str2Buffer(hdr.magic), 344)
498
+ view.setInt32(344, 3222382, true) // "n+1\0"
499
+
481
500
  return byteArray
482
501
  // return byteArray.buffer;
483
502
  }
@@ -1171,7 +1171,8 @@ export class NVMeshLoaders {
1171
1171
  const view = new DataView(buffer) // ArrayBuffer to dataview
1172
1172
  // ALWAYS big endian
1173
1173
  const n_vertex = view.getUint32(0, false)
1174
- if (n_vert !== n_vertex) {
1174
+ const n_vertexDecimated = this.decimateLayerVertices(n_vertex, n_vert)
1175
+ if (n_vert !== n_vertexDecimated) {
1175
1176
  throw new Error('ANNOT file has different number of vertices than mesh')
1176
1177
  }
1177
1178
  if (buffer.byteLength < 4 + 8 * n_vertex) {
@@ -2505,6 +2506,23 @@ export class NVMeshLoaders {
2505
2506
  }
2506
2507
  } // readSTL()
2507
2508
 
2509
+ static decimateLayerVertices(nVertLayer: number, nVertMesh: number): number {
2510
+ // downsample layer vertices if the mesh has been decimated
2511
+ if (nVertLayer % nVertMesh === 0) {
2512
+ return nVertLayer
2513
+ }
2514
+ const V0 = 12
2515
+ const orderLayer = Math.round(Math.log((nVertLayer - 2) / (V0 - 2)) / Math.log(4))
2516
+ const orderMesh = Math.round(Math.log((nVertMesh - 2) / (V0 - 2)) / Math.log(4))
2517
+ // sanity check
2518
+ const nVLayer = Math.pow(4, orderLayer) * (V0 - 2) + 2
2519
+ const nVMesh = Math.pow(4, orderMesh) * (V0 - 2) + 2
2520
+ if (nVLayer !== nVertLayer || nVMesh !== nVertMesh) {
2521
+ return nVertLayer
2522
+ }
2523
+ return nVertMesh
2524
+ }
2525
+
2508
2526
  // read NIfTI2 format with embedded CIfTI
2509
2527
  // this variation very specific to connectome workbench
2510
2528
  // https://brainder.org/2015/04/03/the-nifti-2-file-format/
@@ -2688,8 +2706,9 @@ export class NVMeshLoaders {
2688
2706
  //
2689
2707
  return scalars
2690
2708
  } // is CIfTI
2709
+ nvert = this.decimateLayerVertices(nvert, n_vert)
2691
2710
  if (nvert % n_vert !== 0) {
2692
- throw new Error('Vertices in NIfTI (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
2711
+ throw new Error('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
2693
2712
  }
2694
2713
  if (isLittleEndian) {
2695
2714
  // block read native endian
@@ -2776,8 +2795,9 @@ export class NVMeshLoaders {
2776
2795
  const dim = reader.getUint16(40 + i * 2, isLittleEndian)
2777
2796
  nvert *= Math.max(dim, 1)
2778
2797
  }
2798
+ nvert = this.decimateLayerVertices(nvert, n_vert)
2779
2799
  if (nvert % n_vert !== 0) {
2780
- throw new Error('Vertices in NIfTI (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
2800
+ throw new Error('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
2781
2801
  }
2782
2802
  if (isLittleEndian) {
2783
2803
  // block read native endian
@@ -2835,10 +2855,11 @@ export class NVMeshLoaders {
2835
2855
  if (version !== 1 || mtype < 0 || mtype > 4) {
2836
2856
  log.warn('Not a valid MGH file')
2837
2857
  }
2838
- const nvert = width * height * depth * nframes
2858
+ let nvert = width * height * depth * nframes
2839
2859
  let scalars: AnyNumberArray = []
2860
+ nvert = this.decimateLayerVertices(nvert, n_vert)
2840
2861
  if (nvert % n_vert !== 0) {
2841
- log.warn('Vertices in NIfTI (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
2862
+ log.warn('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
2842
2863
  return scalars
2843
2864
  }
2844
2865
  if (mtype === 3) {
@@ -1,4 +1,5 @@
1
1
  import { vec3 } from 'gl-matrix'
2
+ import { gzipSync } from 'fflate/browser'
2
3
 
3
4
  type Extents = {
4
5
  mxDx: number
@@ -38,6 +39,144 @@ export class NVMeshUtilities {
38
39
  return border
39
40
  }
40
41
 
42
+ static createMZ3(vertices: Float32Array, indices: Uint32Array, compress: boolean = false): ArrayBuffer {
43
+ // generate binary MZ3 format mesh
44
+ // n.b. small, precise and small but support is not widespread
45
+ // n.b. result can be compressed with gzip
46
+ // https://github.com/neurolabusc/surf-ice/tree/master/mz3
47
+ const magic = 23117
48
+ const attr = 3
49
+ const nface = indices.length / 3
50
+ const nvert = vertices.length / 3
51
+ const nskip = 0
52
+ // Calculate buffer size
53
+ const headerSize = 16
54
+ const indexSize = nface * 3 * 4 // Uint32Array
55
+ const vertexSize = nvert * 3 * 4 // Float32Array
56
+ const totalSize = headerSize + indexSize + vertexSize
57
+ const buffer = new ArrayBuffer(totalSize)
58
+ const writer = new DataView(buffer)
59
+ // Write header
60
+ writer.setUint16(0, magic, true)
61
+ writer.setUint16(2, attr, true)
62
+ writer.setUint32(4, nface, true)
63
+ writer.setUint32(8, nvert, true)
64
+ writer.setUint32(12, nskip, true)
65
+ // Write indices
66
+ let offset = headerSize
67
+ new Uint32Array(buffer, offset, indices.length).set(indices)
68
+ offset += indexSize
69
+ // Write vertices
70
+ new Float32Array(buffer, offset, vertices.length).set(vertices)
71
+ if (compress) {
72
+ return gzipSync(new Uint8Array(buffer))
73
+ }
74
+ return buffer
75
+ }
76
+
77
+ static createOBJ(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {
78
+ // generate binary OBJ format mesh
79
+ // n.b. widespread support, but large and slow due to ASCII
80
+ // https://en.wikipedia.org/wiki/Wavefront_.obj_file
81
+ let objContent = ''
82
+ // Add vertices to OBJ content
83
+ for (let i = 0; i < vertices.length; i += 3) {
84
+ objContent += `v ${vertices[i]} ${vertices[i + 1]} ${vertices[i + 2]}\n`
85
+ }
86
+ // Add faces to OBJ content (OBJ indices start at 1, not 0)
87
+ for (let i = 0; i < indices.length; i += 3) {
88
+ objContent += `f ${indices[i] + 1} ${indices[i + 1] + 1} ${indices[i + 2] + 1}\n`
89
+ }
90
+ // Encode the OBJ content as an ArrayBuffer
91
+ const encoder = new TextEncoder()
92
+ const arrayBuffer = encoder.encode(objContent).buffer
93
+ return arrayBuffer
94
+ }
95
+
96
+ static createSTL(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {
97
+ // generate binary STL format mesh
98
+ // n.b. inefficient and slow as vertices are not reused
99
+ // https://en.wikipedia.org/wiki/STL_(file_format)#Binary
100
+ const numTriangles = indices.length / 3
101
+ const bufferLength = 84 + numTriangles * 50
102
+ const arrayBuffer = new ArrayBuffer(bufferLength)
103
+ const dataView = new DataView(arrayBuffer)
104
+ // Write header (80 bytes)
105
+ for (let i = 0; i < 80; i++) {
106
+ dataView.setUint8(i, 0)
107
+ }
108
+ // Write number of triangles (4 bytes)
109
+ dataView.setUint32(80, numTriangles, true)
110
+ let offset = 84
111
+ for (let i = 0; i < indices.length; i += 3) {
112
+ const i0 = indices[i] * 3
113
+ const i1 = indices[i + 1] * 3
114
+ const i2 = indices[i + 2] * 3
115
+ // Normal vector (12 bytes, set to zero)
116
+ dataView.setFloat32(offset, 0, true) // Normal X
117
+ dataView.setFloat32(offset + 4, 0, true) // Normal Y
118
+ dataView.setFloat32(offset + 8, 0, true) // Normal Z
119
+ offset += 12
120
+ // Vertex 1 (12 bytes)
121
+ dataView.setFloat32(offset, vertices[i0], true) // Vertex 1 X
122
+ dataView.setFloat32(offset + 4, vertices[i0 + 1], true) // Vertex 1 Y
123
+ dataView.setFloat32(offset + 8, vertices[i0 + 2], true) // Vertex 1 Z
124
+ offset += 12
125
+ // Vertex 2 (12 bytes)
126
+ dataView.setFloat32(offset, vertices[i1], true) // Vertex 2 X
127
+ dataView.setFloat32(offset + 4, vertices[i1 + 1], true) // Vertex 2 Y
128
+ dataView.setFloat32(offset + 8, vertices[i1 + 2], true) // Vertex 2 Z
129
+ offset += 12
130
+ // Vertex 3 (12 bytes)
131
+ dataView.setFloat32(offset, vertices[i2], true) // Vertex 3 X
132
+ dataView.setFloat32(offset + 4, vertices[i2 + 1], true) // Vertex 3 Y
133
+ dataView.setFloat32(offset + 8, vertices[i2 + 2], true) // Vertex 3 Z
134
+ offset += 12
135
+ // Attribute byte count (2 bytes, set to zero)
136
+ dataView.setUint16(offset, 0, true)
137
+ offset += 2
138
+ }
139
+ return arrayBuffer
140
+ }
141
+
142
+ static downloadArrayBuffer(buffer: ArrayBuffer, filename: string): void {
143
+ const blob = new Blob([buffer], { type: 'application/octet-stream' })
144
+ const url = URL.createObjectURL(blob)
145
+ const a = document.createElement('a')
146
+ a.href = url
147
+ a.download = filename
148
+ document.body.appendChild(a)
149
+ a.style.display = 'none'
150
+ a.click()
151
+ setTimeout(() => {
152
+ document.body.removeChild(a)
153
+ URL.revokeObjectURL(url)
154
+ }, 0)
155
+ }
156
+
157
+ static saveMesh(
158
+ vertices: Float32Array,
159
+ indices: Uint32Array,
160
+ filename: string = '.mz3',
161
+ compress: boolean = false
162
+ ): ArrayBuffer {
163
+ let buff = new ArrayBuffer(0)
164
+ if (/\.obj$/i.test(filename)) {
165
+ buff = this.createOBJ(vertices, indices)
166
+ } else if (/\.stl$/i.test(filename)) {
167
+ buff = this.createSTL(vertices, indices)
168
+ } else {
169
+ if (!/\.mz3$/i.test(filename)) {
170
+ filename += '.mz3'
171
+ }
172
+ buff = this.createMZ3(vertices, indices, compress)
173
+ }
174
+ if (filename.length > 4) {
175
+ this.downloadArrayBuffer(buff, filename)
176
+ }
177
+ return buff
178
+ }
179
+
41
180
  static getClusterBoundary(rgba8: Uint8Array, faces: number[] | Uint32Array): boolean[] {
42
181
  const rgba32 = new Uint32Array(rgba8.buffer)
43
182
  // assume all vertices are not near a border