@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/build/index.d.ts +35 -2
- package/build/index.js +363 -58
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +35 -2
- package/dist/index.js +25866 -30696
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/niivue.umd.js +72 -32
- package/package.json +18 -17
- package/src/niivue/index.ts +32 -2
- package/src/nvimage/index.ts +25 -2
- package/src/nvimage/utils.ts +61 -42
- package/src/nvmesh-loaders.ts +26 -5
- package/src/nvmesh-utilities.ts +139 -0
- package/src/nvmesh.ts +96 -0
- package/src/shader-srcs.ts +43 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niivue/niivue",
|
|
3
|
-
"version": "0.44.
|
|
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.
|
|
76
|
-
"@rollup/plugin-commonjs": "^
|
|
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.
|
|
78
|
+
"@types/node": "^20.14.11",
|
|
79
79
|
"@types/ungap__structured-clone": "^1.2.0",
|
|
80
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
81
|
-
"@typescript-eslint/parser": "^7.
|
|
82
|
-
"@vitest/coverage-v8": "^
|
|
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
|
|
87
|
-
"eslint-plugin-tsdoc": "^0.
|
|
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
|
|
89
|
+
"happy-dom": "^14.12.3",
|
|
90
90
|
"http-server": "^14.1.1",
|
|
91
|
-
"prettier": "^3.
|
|
91
|
+
"prettier": "^3.3.3",
|
|
92
92
|
"regenerator-runtime": "^0.14.1",
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
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.
|
|
101
|
+
"@rollup/rollup-linux-x64-gnu": "^4.18.1"
|
|
101
102
|
}
|
|
102
103
|
}
|
package/src/niivue/index.ts
CHANGED
|
@@ -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
|
package/src/nvimage/index.ts
CHANGED
|
@@ -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 (
|
|
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
|
|
package/src/nvimage/utils.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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],
|
|
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,
|
|
408
|
-
view.setFloat32(60, hdr.intent_p2,
|
|
409
|
-
view.setFloat32(64, hdr.intent_p3,
|
|
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,
|
|
418
|
+
view.setInt16(68, hdr.intent_code, isLittleEndian)
|
|
412
419
|
if (isDrawing8) {
|
|
413
|
-
view.setInt16(70, 2,
|
|
414
|
-
view.setInt16(72, 8,
|
|
420
|
+
view.setInt16(70, 2, isLittleEndian) // 2 = DT_UINT8
|
|
421
|
+
view.setInt16(72, 8, isLittleEndian)
|
|
415
422
|
} else {
|
|
416
|
-
view.setInt16(70, hdr.datatypeCode,
|
|
417
|
-
view.setInt16(72, hdr.numBitsPerVoxel,
|
|
423
|
+
view.setInt16(70, hdr.datatypeCode, isLittleEndian)
|
|
424
|
+
view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian)
|
|
418
425
|
}
|
|
419
|
-
view.setInt16(74, hdr.slice_start,
|
|
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],
|
|
430
|
+
view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian)
|
|
424
431
|
}
|
|
425
432
|
if (isDrawing8) {
|
|
426
|
-
view.setFloat32(108, 352,
|
|
427
|
-
view.setFloat32(112, 1.0,
|
|
428
|
-
view.setFloat32(116, 0.0,
|
|
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,
|
|
431
|
-
view.setFloat32(
|
|
432
|
-
view.setFloat32(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
444
|
-
view.setFloat32(128, 0,
|
|
455
|
+
view.setFloat32(124, 0, isLittleEndian)
|
|
456
|
+
view.setFloat32(128, 0, isLittleEndian)
|
|
445
457
|
} else {
|
|
446
|
-
view.setFloat32(124, hdr.cal_max,
|
|
447
|
-
view.setFloat32(128, hdr.cal_min,
|
|
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,
|
|
450
|
-
view.setFloat32(136, hdr.toffset,
|
|
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,
|
|
461
|
-
|
|
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,
|
|
465
|
-
view.setFloat32(260, hdr.quatern_c,
|
|
466
|
-
view.setFloat32(264, hdr.quatern_d,
|
|
467
|
-
view.setFloat32(268, hdr.qoffset_x,
|
|
468
|
-
view.setFloat32(272, hdr.qoffset_y,
|
|
469
|
-
view.setFloat32(276, hdr.qoffset_z,
|
|
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],
|
|
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
|
}
|
package/src/nvmesh-loaders.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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) {
|
package/src/nvmesh-utilities.ts
CHANGED
|
@@ -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
|