@niivue/niivue 0.48.0 → 0.49.0

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.js CHANGED
@@ -10,7 +10,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
10
10
  import { mat4 as mat46, vec2, vec3 as vec311, vec4 as vec48 } from "gl-matrix";
11
11
 
12
12
  // package.json
13
- var version = "0.48.0";
13
+ var version = "0.49.0";
14
14
 
15
15
  // src/logger.ts
16
16
  var _Log = class _Log {
@@ -83,6 +83,13 @@ var compileShader = function(gl, vert, frag) {
83
83
  gl.attachShader(program, fs);
84
84
  gl.linkProgram(program);
85
85
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
86
+ console.log(gl.getProgramInfoLog(program));
87
+ if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
88
+ console.log("Vertex shader compilation error:", gl.getShaderInfoLog(vs));
89
+ }
90
+ if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
91
+ console.log("Fragment shader compilation error:", gl.getShaderInfoLog(fs));
92
+ }
86
93
  log.error(gl.getProgramInfoLog(program));
87
94
  throw new Error("Shader failed to link, see console for log");
88
95
  }
@@ -1114,6 +1121,9 @@ void main(void) {
1114
1121
  float r = max(0.00001, abs(mx - mn));
1115
1122
  mn = min(mn, mx);
1116
1123
  float txl = mix(0.0, 1.0, (f - mn) / r);
1124
+ if (f > mn) { //issue1139: survives threshold, so round up to opaque voxel
1125
+ txl = max(txl, 2.0/256.0);
1126
+ }
1117
1127
  //https://stackoverflow.com/questions/5879403/opengl-texture-coordinates-in-pixel-space
1118
1128
  float nlayer = float(textureSize(colormap, 0).y);
1119
1129
  //each volume has two color maps:
@@ -1130,6 +1140,8 @@ void main(void) {
1130
1140
  r = max(0.00001, abs(mx - mn));
1131
1141
  mn = min(mn, mx);
1132
1142
  txl = 1.0 - mix(0.0, 1.0, (f - mn) / r);
1143
+ //issue1139: survives threshold, so round up to opaque voxel
1144
+ txl = max(txl, 2.0/256.0);
1133
1145
  y = ((2.0 * layer) + 0.5)/nlayer;
1134
1146
  FragColor = texture(colormap, vec2(txl, y));
1135
1147
  }
@@ -1308,9 +1320,11 @@ uniform mat4 mvpMtx;
1308
1320
  uniform mat4 normMtx;
1309
1321
  out vec4 vClr;
1310
1322
  out vec3 vN;
1323
+ out vec4 vP;
1311
1324
  out vec4 vPc;
1312
1325
  void main(void) {
1313
1326
  vec3 lightPosition = vec3(0.0, 0.0, -10.0);
1327
+ vP = vec4(pos, 1.0);
1314
1328
  vPc = mvpMtx * vec4(pos, 1.0);
1315
1329
  gl_Position = vPc;
1316
1330
  vN = normalize((normMtx * vec4(norm.xyz,1.0)).xyz);
@@ -11169,7 +11183,9 @@ var ColorTables = class {
11169
11183
  const nLabels = cm.R.length;
11170
11184
  const idxs = cm.I ?? [...Array(nLabels).keys()];
11171
11185
  if (nLabels !== cm.G.length || nLabels !== cm.B.length || nLabels !== idxs.length) {
11172
- throw new Error(`colormap does not make sense: ${cm}`);
11186
+ throw new Error(
11187
+ `colormap does not make sense: ${cm} Rs ${cm.R.length} Gs ${cm.G.length} Bs ${cm.B.length} Is ${idxs.length}`
11188
+ );
11173
11189
  }
11174
11190
  let As = new Uint8ClampedArray(nLabels).fill(alphaFill);
11175
11191
  As[0] = 0;
@@ -11656,6 +11672,15 @@ var NVUtilities = class _NVUtilities {
11656
11672
  return mat;
11657
11673
  }
11658
11674
  // readMatV4()
11675
+ static b64toUint8(base64) {
11676
+ const binaryString = atob(base64);
11677
+ const length = binaryString.length;
11678
+ const bytes = new Uint8Array(length);
11679
+ for (let i = 0; i < length; i++) {
11680
+ bytes[i] = binaryString.charCodeAt(i);
11681
+ }
11682
+ return bytes;
11683
+ }
11659
11684
  /*
11660
11685
  https://gist.github.com/jonleighton/958841
11661
11686
  MIT LICENSE
@@ -12559,7 +12584,8 @@ var NVMeshLoaders = class _NVMeshLoaders {
12559
12584
  alphaThreshold: false,
12560
12585
  isTransparentBelowCalMin: true,
12561
12586
  isAdditiveBlend: false,
12562
- colorbarVisible: true
12587
+ colorbarVisible: true,
12588
+ colormapLabel: null
12563
12589
  };
12564
12590
  const isReadColortables = true;
12565
12591
  const re = /(?:\.([^.]+))?$/;
@@ -15456,7 +15482,7 @@ var NVMesh2 = class _NVMesh {
15456
15482
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
15457
15483
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(idxs), gl.STATIC_DRAW);
15458
15484
  gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
15459
- gl.bufferData(gl.ARRAY_BUFFER, Float32Array.from(f32), gl.STATIC_DRAW);
15485
+ gl.bufferData(gl.ARRAY_BUFFER, u8, gl.STATIC_DRAW);
15460
15486
  this.indexCount = nidx;
15461
15487
  }
15462
15488
  // linesToCylinders
@@ -16162,7 +16188,7 @@ var NVMesh2 = class _NVMesh {
16162
16188
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
16163
16189
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Uint32Array.from(this.tris), gl.STATIC_DRAW);
16164
16190
  gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
16165
- gl.bufferData(gl.ARRAY_BUFFER, Float32Array.from(posNormClr), gl.STATIC_DRAW);
16191
+ gl.bufferData(gl.ARRAY_BUFFER, u8, gl.STATIC_DRAW);
16166
16192
  this.indexCount = this.tris.length;
16167
16193
  this.vertexCount = this.pts.length;
16168
16194
  }
@@ -16270,13 +16296,26 @@ var NVMesh2 = class _NVMesh {
16270
16296
  }
16271
16297
  // adjust attributes of a mesh layer. invoked by niivue.setMeshLayerProperty()
16272
16298
  // TODO this method is a bit too generic
16273
- setLayerProperty(id, key, val, gl) {
16299
+ async setLayerProperty(id, key, val, gl) {
16274
16300
  const layer = this.layers[id];
16275
16301
  if (!layer || !(key in layer)) {
16276
16302
  log.warn("mesh does not have property ", key, " for layer ", layer);
16277
16303
  return;
16278
16304
  }
16279
- layer[key] = val;
16305
+ if (key === "colormapLabel") {
16306
+ if (typeof val === "object") {
16307
+ layer[key] = cmapper.makeLabelLut(val);
16308
+ } else if (typeof val === "string") {
16309
+ const cmap = await cmapper.makeLabelLutFromUrl(val);
16310
+ layer[key] = cmap;
16311
+ this.updateMesh(gl);
16312
+ return;
16313
+ } else {
16314
+ log.error("colormapLabel requires a string or object");
16315
+ }
16316
+ } else {
16317
+ layer[key] = val;
16318
+ }
16280
16319
  this.updateMesh(gl);
16281
16320
  }
16282
16321
  // adjust mesh attributes. invoked by niivue.setMeshProperty(()
@@ -16502,8 +16541,13 @@ var NVMesh2 = class _NVMesh {
16502
16541
  anatomicalStructurePrimary
16503
16542
  );
16504
16543
  if ("scalars" in obj && obj.scalars.length > 0) {
16505
- NVMeshLoaders.readLayer(name, buffer, nvm, opacity, "gray");
16506
- nvm.updateMesh(gl);
16544
+ const newLayer = NVMeshLoaders.readLayer(name, buffer, nvm, opacity, "gray");
16545
+ if (typeof newLayer === "undefined") {
16546
+ log.warn("readLayer() failed to convert scalars");
16547
+ } else {
16548
+ nvm.layers.push(newLayer);
16549
+ nvm.updateMesh(gl);
16550
+ }
16507
16551
  }
16508
16552
  return nvm;
16509
16553
  }
@@ -18321,7 +18365,6 @@ import { vec3 as vec38, vec4 as vec46 } from "gl-matrix";
18321
18365
 
18322
18366
  // src/nvimage/index.ts
18323
18367
  import * as nifti from "nifti-reader-js";
18324
- import daikon from "daikon";
18325
18368
  import { mat3, mat4 as mat44, vec3 as vec37, vec4 as vec45 } from "gl-matrix";
18326
18369
  import { Decompress, decompressSync as decompressSync3, gzipSync as gzipSync2 } from "fflate/browser";
18327
18370
  import { v4 as uuidv42 } from "@lukeed/uuid";
@@ -18440,32 +18483,6 @@ var NVImageFromUrlOptions = (url, urlImageData = "", name = "", colormap = "gray
18440
18483
  colormapLabel
18441
18484
  };
18442
18485
  };
18443
- function getBestTransform(imageDirections, voxelDimensions, imagePosition) {
18444
- const cosines = imageDirections;
18445
- let m = null;
18446
- if (cosines) {
18447
- const vs = {
18448
- colSize: voxelDimensions[0],
18449
- rowSize: voxelDimensions[1],
18450
- sliceSize: voxelDimensions[2]
18451
- };
18452
- const coord = imagePosition;
18453
- const cosx = [cosines[0], cosines[1], cosines[2]];
18454
- const cosy = [cosines[3], cosines[4], cosines[5]];
18455
- const cosz = [
18456
- cosx[1] * cosy[2] - cosx[2] * cosy[1],
18457
- cosx[2] * cosy[0] - cosx[0] * cosy[2],
18458
- cosx[0] * cosy[1] - cosx[1] * cosy[0]
18459
- ];
18460
- m = [
18461
- [cosx[0] * vs.colSize * -1, cosy[0] * vs.rowSize * -1, cosz[0] * vs.sliceSize * -1, -1 * coord[0]],
18462
- [cosx[1] * vs.colSize * -1, cosy[1] * vs.rowSize * -1, cosz[1] * vs.sliceSize * -1, -1 * coord[1]],
18463
- [cosx[2] * vs.colSize, cosy[2] * vs.rowSize, cosz[2] * vs.sliceSize, coord[2]],
18464
- [0, 0, 0, 1]
18465
- ];
18466
- }
18467
- return m;
18468
- }
18469
18486
  function str2Buffer(str, maxLen = 80) {
18470
18487
  str = str.replace(/[`$]/g, "");
18471
18488
  const bytes = [];
@@ -18766,8 +18783,7 @@ var NVImage = class _NVImage {
18766
18783
  case NVIMAGE_TYPE.DCM_FOLDER:
18767
18784
  case NVIMAGE_TYPE.DCM_MANIFEST:
18768
18785
  case NVIMAGE_TYPE.DCM:
18769
- imgRaw = this.readDICOM(dataBuffer);
18770
- break;
18786
+ return;
18771
18787
  case NVIMAGE_TYPE.FIB:
18772
18788
  ;
18773
18789
  [imgRaw, this.v1] = this.readFIB(dataBuffer);
@@ -19311,121 +19327,133 @@ var NVImage = class _NVImage {
19311
19327
  }
19312
19328
  // not included in public docs
19313
19329
  // read DICOM format image and treat it like a NIfTI
19314
- readDICOM(buf) {
19315
- this.series = new daikon.Series();
19316
- if (Array.isArray(buf)) {
19317
- for (let i = 0; i < buf.length; i++) {
19318
- const dataview = new DataView(buf[i]);
19319
- const image = daikon.Series.parseImage(dataview);
19320
- if (image === null) {
19321
- log.error(daikon.Series.parserError);
19322
- } else if (image.hasPixelData()) {
19323
- if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {
19324
- this.series.addImage(image);
19325
- }
19326
- }
19327
- }
19328
- } else {
19329
- const image = daikon.Series.parseImage(new DataView(buf));
19330
- if (image === null) {
19331
- log.error(daikon.Series.parserError);
19332
- } else if (image.hasPixelData()) {
19333
- if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {
19334
- this.series.addImage(image);
19335
- }
19336
- }
19337
- }
19338
- this.series.buildSeries();
19339
- this.hdr = new nifti.NIFTI1();
19340
- const hdr = this.hdr;
19341
- hdr.scl_inter = 0;
19342
- hdr.scl_slope = 1;
19343
- if (this.series.images[0].getDataScaleIntercept()) {
19344
- hdr.scl_inter = this.series.images[0].getDataScaleIntercept();
19345
- }
19346
- if (this.series.images[0].getDataScaleSlope()) {
19347
- hdr.scl_slope = this.series.images[0].getDataScaleSlope();
19348
- }
19349
- hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
19350
- hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
19351
- hdr.dims[1] = this.series.images[0].getCols();
19352
- hdr.dims[2] = this.series.images[0].getRows();
19353
- hdr.dims[3] = this.series.images[0].getNumberOfFrames();
19354
- if (this.series.images.length > 1) {
19355
- if (hdr.dims[3] > 1) {
19356
- log.debug("To Do: multiple slices per file and multiple files (XA30 DWI)");
19357
- }
19358
- hdr.dims[3] = this.series.images.length;
19359
- }
19360
- const rc = this.series.images[0].getPixelSpacing();
19361
- hdr.pixDims[1] = rc[0];
19362
- hdr.pixDims[2] = rc[1];
19363
- if (this.series.images.length > 1) {
19364
- const p0 = vec37.fromValues(...this.series.images[0].getImagePosition());
19365
- const p1 = vec37.fromValues(...this.series.images[1].getImagePosition());
19366
- const n = vec37.fromValues(0, 0, 0);
19367
- vec37.subtract(n, p0, p1);
19368
- hdr.pixDims[3] = vec37.length(n);
19369
- } else {
19370
- hdr.pixDims[3] = this.series.images[0].getSliceThickness();
19371
- }
19372
- hdr.pixDims[4] = this.series.images[0].getTR() / 1e3;
19373
- const dt = this.series.images[0].getDataType();
19374
- const bpv = this.series.images[0].getBitsAllocated();
19375
- hdr.numBitsPerVoxel = bpv;
19376
- this.hdr.littleEndian = this.series.images[0].littleEndian;
19377
- if (bpv === 8 && dt === 2) {
19378
- hdr.datatypeCode = 256 /* DT_INT8 */;
19379
- } else if (bpv === 8 && dt === 3) {
19380
- hdr.datatypeCode = 2 /* DT_UINT8 */;
19381
- } else if (bpv === 16 && dt === 2) {
19382
- hdr.datatypeCode = 4 /* DT_INT16 */;
19383
- } else if (bpv === 16 && dt === 3) {
19384
- hdr.datatypeCode = 512 /* DT_UINT16 */;
19385
- } else if (bpv === 32 && dt === 2) {
19386
- hdr.datatypeCode = 8 /* DT_INT32 */;
19387
- } else if (bpv === 32 && dt === 3) {
19388
- hdr.datatypeCode = 768 /* DT_UINT32 */;
19389
- } else if (bpv === 32 && dt === 4) {
19390
- hdr.datatypeCode = 16 /* DT_FLOAT32 */;
19391
- } else if (bpv === 64 && dt === 4) {
19392
- hdr.datatypeCode = 64 /* DT_FLOAT64 */;
19393
- } else if (bpv === 1) {
19394
- hdr.datatypeCode = 1 /* DT_BINARY */;
19395
- } else {
19396
- log.warn("Unsupported DICOM format: " + dt + " " + bpv);
19397
- }
19398
- const voxelDimensions = hdr.pixDims.slice(1, 4);
19399
- const m = getBestTransform(
19400
- this.series.images[0].getImageDirections(),
19401
- voxelDimensions,
19402
- this.series.images[0].getImagePosition()
19403
- );
19404
- if (m) {
19405
- hdr.sform_code = 1;
19406
- hdr.affine = [
19407
- [m[0][0], m[0][1], m[0][2], m[0][3]],
19408
- [m[1][0], m[1][1], m[1][2], m[1][3]],
19409
- [m[2][0], m[2][1], m[2][2], m[2][3]],
19410
- [0, 0, 0, 1]
19411
- ];
19412
- }
19413
- let data;
19414
- let length = this.series.validatePixelDataLength(this.series.images[0]);
19415
- const buffer = new Uint8Array(new ArrayBuffer(length * this.series.images.length));
19416
- for (let i = 0; i < this.series.images.length; i++) {
19417
- if (this.series.isMosaic) {
19418
- data = this.series.getMosaicData(this.series.images[i], this.series.images[i].getPixelDataBytes());
19419
- } else {
19420
- data = this.series.images[i].getPixelDataBytes();
19421
- }
19422
- length = this.series.validatePixelDataLength(this.series.images[i]);
19423
- this.series.images[i].clearPixelData();
19424
- buffer.set(new Uint8Array(data, 0, length), length * i);
19425
- }
19426
- return buffer.buffer;
19427
- }
19428
- // readDICOM()
19330
+ // -----------------
19331
+ // readDICOM(buf: ArrayBuffer | ArrayBuffer[]): ArrayBuffer {
19332
+ // this.series = new daikon.Series()
19333
+ // // parse DICOM file
19334
+ // if (Array.isArray(buf)) {
19335
+ // for (let i = 0; i < buf.length; i++) {
19336
+ // const dataview = new DataView(buf[i])
19337
+ // const image = daikon.Series.parseImage(dataview)
19338
+ // if (image === null) {
19339
+ // log.error(daikon.Series.parserError)
19340
+ // } else if (image.hasPixelData()) {
19341
+ // // if it's part of the same series, add it
19342
+ // if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {
19343
+ // this.series.addImage(image)
19344
+ // }
19345
+ // } // if hasPixelData
19346
+ // } // for i
19347
+ // } else {
19348
+ // // not a dicom folder drop
19349
+ // const image = daikon.Series.parseImage(new DataView(buf))
19350
+ // if (image === null) {
19351
+ // log.error(daikon.Series.parserError)
19352
+ // } else if (image.hasPixelData()) {
19353
+ // // if it's part of the same series, add it
19354
+ // if (this.series.images.length === 0 || image.getSeriesId() === this.series.images[0].getSeriesId()) {
19355
+ // this.series.addImage(image)
19356
+ // }
19357
+ // }
19358
+ // }
19359
+ // // order the image files, determines number of frames, etc.
19360
+ // this.series.buildSeries()
19361
+ // // output some header info
19362
+ // this.hdr = new nifti.NIFTI1()
19363
+ // const hdr = this.hdr
19364
+ // hdr.scl_inter = 0
19365
+ // hdr.scl_slope = 1
19366
+ // if (this.series.images[0].getDataScaleIntercept()) {
19367
+ // hdr.scl_inter = this.series.images[0].getDataScaleIntercept()
19368
+ // }
19369
+ // if (this.series.images[0].getDataScaleSlope()) {
19370
+ // hdr.scl_slope = this.series.images[0].getDataScaleSlope()
19371
+ // }
19372
+ // hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0]
19373
+ // hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0]
19374
+ // hdr.dims[1] = this.series.images[0].getCols()
19375
+ // hdr.dims[2] = this.series.images[0].getRows()
19376
+ // hdr.dims[3] = this.series.images[0].getNumberOfFrames()
19377
+ // if (this.series.images.length > 1) {
19378
+ // if (hdr.dims[3] > 1) {
19379
+ // log.debug('To Do: multiple slices per file and multiple files (XA30 DWI)')
19380
+ // }
19381
+ // hdr.dims[3] = this.series.images.length
19382
+ // }
19383
+ // const rc = this.series.images[0].getPixelSpacing() // TODO: order?
19384
+ // hdr.pixDims[1] = rc[0]
19385
+ // hdr.pixDims[2] = rc[1]
19386
+ // if (this.series.images.length > 1) {
19387
+ // // Multiple slices. The depth of a pixel is the physical distance between offsets. This is not the same as slice
19388
+ // // spacing for tilted slices (skew).
19389
+ // const p0 = vec3.fromValues(...(this.series.images[0].getImagePosition() as [number, number, number]))
19390
+ // const p1 = vec3.fromValues(...(this.series.images[1].getImagePosition() as [number, number, number]))
19391
+ // const n = vec3.fromValues(0, 0, 0)
19392
+ // vec3.subtract(n, p0, p1)
19393
+ // hdr.pixDims[3] = vec3.length(n)
19394
+ // } else {
19395
+ // // Single slice. Use the slice thickness as pixel depth.
19396
+ // hdr.pixDims[3] = this.series.images[0].getSliceThickness()
19397
+ // }
19398
+ // hdr.pixDims[4] = this.series.images[0].getTR() / 1000.0 // msec -> sec
19399
+ // const dt = this.series.images[0].getDataType() // 2=int,3=uint,4=float,
19400
+ // const bpv = this.series.images[0].getBitsAllocated()
19401
+ // hdr.numBitsPerVoxel = bpv
19402
+ // this.hdr.littleEndian = this.series.images[0].littleEndian
19403
+ // if (bpv === 8 && dt === 2) {
19404
+ // hdr.datatypeCode = NiiDataType.DT_INT8
19405
+ // } else if (bpv === 8 && dt === 3) {
19406
+ // hdr.datatypeCode = NiiDataType.DT_UINT8
19407
+ // } else if (bpv === 16 && dt === 2) {
19408
+ // hdr.datatypeCode = NiiDataType.DT_INT16
19409
+ // } else if (bpv === 16 && dt === 3) {
19410
+ // hdr.datatypeCode = NiiDataType.DT_UINT16
19411
+ // } else if (bpv === 32 && dt === 2) {
19412
+ // hdr.datatypeCode = NiiDataType.DT_INT32
19413
+ // } else if (bpv === 32 && dt === 3) {
19414
+ // hdr.datatypeCode = NiiDataType.DT_UINT32
19415
+ // } else if (bpv === 32 && dt === 4) {
19416
+ // hdr.datatypeCode = NiiDataType.DT_FLOAT32
19417
+ // } else if (bpv === 64 && dt === 4) {
19418
+ // hdr.datatypeCode = NiiDataType.DT_FLOAT64
19419
+ // } else if (bpv === 1) {
19420
+ // hdr.datatypeCode = NiiDataType.DT_BINARY
19421
+ // } else {
19422
+ // log.warn('Unsupported DICOM format: ' + dt + ' ' + bpv)
19423
+ // }
19424
+ // const voxelDimensions = hdr.pixDims.slice(1, 4)
19425
+ // const m = getBestTransform(
19426
+ // this.series.images[0].getImageDirections(),
19427
+ // voxelDimensions,
19428
+ // this.series.images[0].getImagePosition()
19429
+ // )
19430
+ // if (m) {
19431
+ // hdr.sform_code = 1
19432
+ // hdr.affine = [
19433
+ // [m[0][0], m[0][1], m[0][2], m[0][3]],
19434
+ // [m[1][0], m[1][1], m[1][2], m[1][3]],
19435
+ // [m[2][0], m[2][1], m[2][2], m[2][3]],
19436
+ // [0, 0, 0, 1]
19437
+ // ]
19438
+ // }
19439
+ // let data
19440
+ // let length = this.series.validatePixelDataLength(this.series.images[0])
19441
+ // const buffer = new Uint8Array(new ArrayBuffer(length * this.series.images.length))
19442
+ // // implementation copied from:
19443
+ // // https://github.com/rii-mango/Daikon/blob/bbe08bad9758dfbdf31ca22fb79048c7bad85706/src/series.js#L496
19444
+ // for (let i = 0; i < this.series.images.length; i++) {
19445
+ // if (this.series.isMosaic) {
19446
+ // data = this.series.getMosaicData(this.series.images[i], this.series.images[i].getPixelDataBytes())
19447
+ // } else {
19448
+ // data = this.series.images[i].getPixelDataBytes()
19449
+ // }
19450
+ // length = this.series.validatePixelDataLength(this.series.images[i])
19451
+ // this.series.images[i].clearPixelData()
19452
+ // buffer.set(new Uint8Array(data, 0, length), length * i)
19453
+ // } // for images.length
19454
+ // return buffer.buffer
19455
+ // } // readDICOM()
19456
+ // -----------------------
19429
19457
  // not included in public docs
19430
19458
  // read ECAT7 format image
19431
19459
  // https://github.com/openneuropet/PET2BIDS/tree/28aae3fab22309047d36d867c624cd629c921ca6/ecat_validation/ecat_info
@@ -21325,7 +21353,10 @@ var NVImage = class _NVImage {
21325
21353
  throw Error(response.statusText);
21326
21354
  }
21327
21355
  const contents = await response.arrayBuffer();
21328
- dataBuffer.push(contents);
21356
+ dataBuffer.push({
21357
+ name: line,
21358
+ data: contents
21359
+ });
21329
21360
  }
21330
21361
  return dataBuffer;
21331
21362
  }
@@ -22459,7 +22490,8 @@ var DEFAULT_OPTIONS = {
22459
22490
  // red
22460
22491
  measureLineColor: [1, 0, 0, 1],
22461
22492
  // red
22462
- measureTextHeight: 0.03
22493
+ measureTextHeight: 0.03,
22494
+ isAlphaClipDark: false
22463
22495
  };
22464
22496
  var INITIAL_SCENE_DATA = {
22465
22497
  gamma: 1,
@@ -22706,55 +22738,12 @@ var NVDocument = class _NVDocument {
22706
22738
  }
22707
22739
  data.customData = this.customData;
22708
22740
  if (this.volumes.length) {
22709
- let imageOptions = this.imageOptionsArray[0];
22710
- if (!imageOptions) {
22711
- log.debug("no image options for base image");
22712
- imageOptions = {
22713
- name: "",
22714
- colormap: "gray",
22715
- opacity: 1,
22716
- pairedImgData: null,
22717
- cal_min: NaN,
22718
- cal_max: NaN,
22719
- trustCalMinMax: true,
22720
- percentileFrac: 0.02,
22721
- ignoreZeroVoxels: false,
22722
- useQFormNotSForm: false,
22723
- colormapNegative: "",
22724
- colormapLabel: null,
22725
- imageType: NVIMAGE_TYPE.NII,
22726
- frame4D: 0,
22727
- limitFrames4D: NaN,
22728
- // TODO the following fields were previously not included
22729
- url: "",
22730
- urlImageData: "",
22731
- alphaThreshold: false,
22732
- cal_minNeg: NaN,
22733
- cal_maxNeg: NaN,
22734
- colorbarVisible: true
22735
- };
22736
- }
22737
- imageOptions.colormap = this.volumes[0].colormap;
22738
- imageOptions.opacity = this.volumes[0].opacity;
22739
- imageOptions.cal_max = this.volumes[0].cal_max || NaN;
22740
- imageOptions.cal_min = this.volumes[0].cal_min || NaN;
22741
- if (imageOptions) {
22742
- imageOptionsArray.push(imageOptions);
22743
- const encodedImageBlob = NVUtilities.uint8tob64(this.volumes[0].toUint8Array());
22744
- data.encodedImageBlobs.push(encodedImageBlob);
22745
- if (this.drawBitmap) {
22746
- data.encodedDrawingBlob = NVUtilities.uint8tob64(this.volumes[0].toUint8Array(this.drawBitmap));
22747
- }
22748
- data.imageOptionsMap.set(this.volumes[0].id, 0);
22749
- } else {
22750
- throw new Error("image options for base layer not found");
22751
- }
22752
- for (let i = 1; i < this.volumes.length; i++) {
22741
+ for (let i = 0; i < this.volumes.length; i++) {
22753
22742
  const volume = this.volumes[i];
22754
- let imageOptions2 = this.getImageOptions(volume);
22755
- if (imageOptions2 === null) {
22743
+ let imageOptions = this.getImageOptions(volume);
22744
+ if (imageOptions === null) {
22756
22745
  log.warn("no options found for image, using default");
22757
- imageOptions2 = {
22746
+ imageOptions = {
22758
22747
  name: "",
22759
22748
  colormap: "gray",
22760
22749
  opacity: 1,
@@ -22779,16 +22768,16 @@ var NVDocument = class _NVDocument {
22779
22768
  colorbarVisible: true
22780
22769
  };
22781
22770
  } else {
22782
- if (!("imageType" in imageOptions2)) {
22783
- imageOptions2.imageType = NVIMAGE_TYPE.NII;
22771
+ if (!("imageType" in imageOptions)) {
22772
+ imageOptions.imageType = NVIMAGE_TYPE.NII;
22784
22773
  }
22785
22774
  }
22786
- imageOptions2.colormap = volume.colormap;
22787
- imageOptions2.colormapLabel = volume.colormapLabel;
22788
- imageOptions2.opacity = volume.opacity;
22789
- imageOptions2.cal_max = volume.cal_max || NaN;
22790
- imageOptions2.cal_min = volume.cal_min || NaN;
22791
- imageOptionsArray.push(imageOptions2);
22775
+ imageOptions.colormap = volume.colormap;
22776
+ imageOptions.colormapLabel = volume.colormapLabel;
22777
+ imageOptions.opacity = volume.opacity;
22778
+ imageOptions.cal_max = volume.cal_max || NaN;
22779
+ imageOptions.cal_min = volume.cal_min || NaN;
22780
+ imageOptionsArray.push(imageOptions);
22792
22781
  const encodedImageBlob = NVUtilities.uint8tob64(volume.toUint8Array());
22793
22782
  data.encodedImageBlobs.push(encodedImageBlob);
22794
22783
  data.imageOptionsMap.set(volume.id, i);
@@ -22857,6 +22846,9 @@ var NVDocument = class _NVDocument {
22857
22846
  meshes.push(copyMesh);
22858
22847
  }
22859
22848
  data.meshesString = JSON.stringify(serialize(meshes));
22849
+ if (this.drawBitmap) {
22850
+ data.encodedDrawingBlob = NVUtilities.uint8tob64(this.drawBitmap);
22851
+ }
22860
22852
  return data;
22861
22853
  }
22862
22854
  /**
@@ -24206,6 +24198,16 @@ var Niivue = class {
24206
24198
  */
24207
24199
  constructor(options = DEFAULT_OPTIONS) {
24208
24200
  __publicField(this, "loaders", {});
24201
+ // create a dicom loader
24202
+ __publicField(this, "dicomLoader", null);
24203
+ // {
24204
+ // loader: (data: DicomLoaderInput) => {
24205
+ // return new Promise<{name: string; data: ArrayBuffer}[]>((resolve, reject) => {
24206
+ // reject('No DICOM loader provided')
24207
+ // })
24208
+ // },
24209
+ // toExt: 'nii'
24210
+ // }
24209
24211
  __publicField(this, "canvas", null);
24210
24212
  // the reference to the canvas element on the page
24211
24213
  __publicField(this, "_gl", null);
@@ -24301,7 +24303,6 @@ var Niivue = class {
24301
24303
  // float, 0 for none
24302
24304
  __publicField(this, "overlayAlphaShader", 1);
24303
24305
  // float, 1 for opaque
24304
- __publicField(this, "isAlphaClipDark", false);
24305
24306
  __publicField(this, "position");
24306
24307
  __publicField(this, "extentsMin");
24307
24308
  __publicField(this, "extentsMax");
@@ -24637,6 +24638,8 @@ var Niivue = class {
24637
24638
  });
24638
24639
  __publicField(this, "onMeshPropertyChanged", () => {
24639
24640
  });
24641
+ __publicField(this, "onDicomLoaderFinishedWithImages", () => {
24642
+ });
24640
24643
  /**
24641
24644
  * callback function to run when the user loads a new NiiVue document
24642
24645
  * @example
@@ -24685,6 +24688,12 @@ var Niivue = class {
24685
24688
  set sliceMosaicString(newSliceMosaicString) {
24686
24689
  this.document.opts.sliceMosaicString = newSliceMosaicString;
24687
24690
  }
24691
+ get isAlphaClipDark() {
24692
+ return this.document.opts.isAlphaClipDark;
24693
+ }
24694
+ set isAlphaClipDark(newVal) {
24695
+ this.document.opts.isAlphaClipDark = newVal;
24696
+ }
24688
24697
  get volumes() {
24689
24698
  return this.document.volumes;
24690
24699
  }
@@ -25661,6 +25670,9 @@ var Niivue = class {
25661
25670
  async addVolumesFromUrl(imageOptionsArray) {
25662
25671
  const promises = imageOptionsArray.map(async (imageItem) => {
25663
25672
  const ext = this.getFileExt(imageItem.name || imageItem.url);
25673
+ if (ext === "DCM") {
25674
+ throw new Error("DICOM files must be loaded using useDicomLoader");
25675
+ }
25664
25676
  if (this.loaders[ext]) {
25665
25677
  let itemToLoad = imageItem.url;
25666
25678
  const toExt = this.loaders[ext].toExt;
@@ -25732,6 +25744,36 @@ var Niivue = class {
25732
25744
  throw new Error("No volume with URL present");
25733
25745
  }
25734
25746
  }
25747
+ async traverseFileTree(item, path = "", fileArray) {
25748
+ return new Promise((resolve) => {
25749
+ if (item.isFile) {
25750
+ item.file((file) => {
25751
+ file.fullPath = path + file.name;
25752
+ file._webkitRelativePath = path + file.name;
25753
+ fileArray.push(file);
25754
+ resolve(fileArray);
25755
+ });
25756
+ } else if (item.isDirectory) {
25757
+ const dirReader = item.createReader();
25758
+ const readAllEntries = () => {
25759
+ dirReader.readEntries((entries) => {
25760
+ if (entries.length > 0) {
25761
+ const promises = [];
25762
+ for (const entry of entries) {
25763
+ promises.push(this.traverseFileTree(entry, path + item.name + "/", fileArray));
25764
+ }
25765
+ Promise.all(promises).then(readAllEntries).catch((e) => {
25766
+ throw e;
25767
+ });
25768
+ } else {
25769
+ resolve(fileArray);
25770
+ }
25771
+ });
25772
+ };
25773
+ readAllEntries();
25774
+ }
25775
+ });
25776
+ }
25735
25777
  readDirectory(directory) {
25736
25778
  const reader = directory.createReader();
25737
25779
  let allEntiresInDir = [];
@@ -25742,6 +25784,7 @@ var Niivue = class {
25742
25784
  };
25743
25785
  for (let i = 0; i < fileSystemEntries.length; i++) {
25744
25786
  allFileObects.push(await getFile(fileSystemEntries[i]));
25787
+ console.log(allFileObects);
25745
25788
  }
25746
25789
  return allFileObects;
25747
25790
  };
@@ -25751,21 +25794,8 @@ var Niivue = class {
25751
25794
  allEntiresInDir = allEntiresInDir.concat(entries);
25752
25795
  readEntries();
25753
25796
  } else {
25754
- getFileObjects(allEntiresInDir).then((allFileObjects) => {
25755
- NVImage.loadFromFile({
25756
- file: allFileObjects,
25757
- // an array of file objects
25758
- name: directory.name,
25759
- urlImgData: null,
25760
- // nothing
25761
- imageType: NVIMAGE_TYPE.DCM_FOLDER
25762
- // signify that this is a dicom directory
25763
- }).then((volume) => {
25764
- this.addVolume(volume);
25765
- this.setDrawingEnabled(true);
25766
- }).catch((e) => {
25767
- throw e;
25768
- });
25797
+ getFileObjects(allEntiresInDir).then(async (allFileObjects) => {
25798
+ console.log(allFileObjects);
25769
25799
  }).catch((e) => {
25770
25800
  throw e;
25771
25801
  });
@@ -25846,6 +25876,17 @@ var Niivue = class {
25846
25876
  })
25847
25877
 
25848
25878
  */
25879
+ // interface LoaderMap {
25880
+ // [key: string]: {
25881
+ // loader: LoaderFunction;
25882
+ // toExt: string;
25883
+ // };
25884
+ // }
25885
+ // Example usage:
25886
+ // const loaderMap: LoaderMap = {
25887
+ // 'TXT': { loader: (file: File) => file.text(), toExt: 'json' }
25888
+ // };
25889
+ // loader can either be a function that takes a file or ArrayBuffer, or the async version of that.
25849
25890
  useLoader(loader, fileExt, toExt) {
25850
25891
  this.loaders = {
25851
25892
  ...this.loaders,
@@ -25855,6 +25896,24 @@ var Niivue = class {
25855
25896
  }
25856
25897
  };
25857
25898
  }
25899
+ useDicomLoader(loader) {
25900
+ this.dicomLoader = loader;
25901
+ }
25902
+ getDicomLoader() {
25903
+ return this.dicomLoader;
25904
+ }
25905
+ // dicom loading is a special case because it can take a list
25906
+ // of files (e.g. from a user supplied DICOM directory) or a single file.
25907
+ // Our preferred DICOM loader is the WASM port of dcm2niix (implemented in a separate niivue loader module).
25908
+ // useDicomLoader(loader: unknown, toExt: string) {
25909
+ // this.loaders = {
25910
+ // ...this.loaders,
25911
+ // ['DCM']: {
25912
+ // loader,
25913
+ // toExt,
25914
+ // },
25915
+ // }
25916
+ // }
25858
25917
  // not included in public docs
25859
25918
  dropListener(e) {
25860
25919
  e.stopPropagation();
@@ -25863,6 +25922,7 @@ var Niivue = class {
25863
25922
  return;
25864
25923
  }
25865
25924
  const urlsToLoad = [];
25925
+ const files = [];
25866
25926
  const dt = e.dataTransfer;
25867
25927
  if (!dt) {
25868
25928
  return;
@@ -25990,7 +26050,33 @@ var Niivue = class {
25990
26050
  }
25991
26051
  });
25992
26052
  } else if (entry.isDirectory) {
25993
- this.readDirectory(entry);
26053
+ this.traverseFileTree(entry, "", files).then((files2) => {
26054
+ const loader = this.getDicomLoader().loader;
26055
+ if (!loader) {
26056
+ throw new Error("No loader for DICOM files");
26057
+ }
26058
+ loader(files2).then(async (fileArrayBuffers) => {
26059
+ console.log(fileArrayBuffers);
26060
+ const promises = fileArrayBuffers.map(
26061
+ (loaderImage) => NVImage.loadFromUrl({
26062
+ url: loaderImage.data,
26063
+ name: loaderImage.name,
26064
+ limitFrames4D: this.opts.limitFrames4D
26065
+ })
26066
+ );
26067
+ Promise.all(promises).then(async (loadedNvImages) => {
26068
+ console.log("from dicom loader");
26069
+ console.log(loadedNvImages);
26070
+ await this.onDicomLoaderFinishedWithImages(loadedNvImages);
26071
+ }).catch((e2) => {
26072
+ throw e2;
26073
+ });
26074
+ }).catch((error) => {
26075
+ console.error("Error loading DICOM files:", error);
26076
+ });
26077
+ }).catch((e2) => {
26078
+ throw e2;
26079
+ });
25994
26080
  }
25995
26081
  }
25996
26082
  }
@@ -26725,13 +26811,13 @@ var Niivue = class {
26725
26811
  * @example niivue.setMeshLayerProperty(niivue.meshes[0].id, 0, 'frame4D', 22)
26726
26812
  * @see {@link https://niivue.github.io/niivue/features/mesh.4D.html | live demo usage}
26727
26813
  */
26728
- setMeshLayerProperty(mesh, layer, key, val) {
26814
+ async setMeshLayerProperty(mesh, layer, key, val) {
26729
26815
  const idx = this.getMeshIndexByID(mesh);
26730
26816
  if (idx < 0) {
26731
26817
  log.warn("setMeshLayerProperty() id not loaded", mesh);
26732
26818
  return;
26733
26819
  }
26734
- this.meshes[idx].setLayerProperty(layer, key, val, this.gl);
26820
+ await this.meshes[idx].setLayerProperty(layer, key, val, this.gl);
26735
26821
  this.updateGLVolume();
26736
26822
  }
26737
26823
  /**
@@ -27304,12 +27390,12 @@ var Niivue = class {
27304
27390
  const encodedImageBlobs = document2.encodedImageBlobs;
27305
27391
  for (let i = 0; i < document2.imageOptionsArray.length; i++) {
27306
27392
  const imageOptions = document2.imageOptionsArray[i];
27307
- const base642 = encodedImageBlobs[i];
27308
- if (base642) {
27393
+ const base64 = encodedImageBlobs[i];
27394
+ if (base64) {
27309
27395
  if ("colorMap" in imageOptions) {
27310
27396
  imageOptions.colormap = imageOptions.colorMap;
27311
27397
  }
27312
- const image = NVImage.loadFromBase64({ base64: base642, ...imageOptions });
27398
+ const image = NVImage.loadFromBase64({ base64, ...imageOptions });
27313
27399
  if (image) {
27314
27400
  if (image.colormapLabel) {
27315
27401
  const length = Object.keys(image.colormapLabel.lut).length;
@@ -27326,14 +27412,6 @@ var Niivue = class {
27326
27412
  if (this.volumes.length > 0) {
27327
27413
  this.back = this.volumes[0];
27328
27414
  }
27329
- const base64 = document2.encodedDrawingBlob;
27330
- if (base64) {
27331
- const imageOptions = document2.imageOptionsArray[0];
27332
- const drawingBitmap = NVImage.loadFromBase64({ base64, ...imageOptions });
27333
- if (drawingBitmap) {
27334
- this.loadDrawing(drawingBitmap);
27335
- }
27336
- }
27337
27415
  for (const meshDataObject of document2.meshDataObjects ?? []) {
27338
27416
  const meshInit = { gl: this.gl, ...meshDataObject };
27339
27417
  if (meshDataObject.offsetPt0) {
@@ -27375,6 +27453,19 @@ var Niivue = class {
27375
27453
  this.addMesh(meshToAdd);
27376
27454
  }
27377
27455
  }
27456
+ this.createEmptyDrawing();
27457
+ const drawingBase64 = document2.encodedDrawingBlob;
27458
+ if (drawingBase64) {
27459
+ const drawingBitmap = NVUtilities.b64toUint8(drawingBase64);
27460
+ if (drawingBitmap) {
27461
+ const dims = this.back.dims;
27462
+ if (drawingBitmap.length !== dims[1] * dims[2] * dims[3]) {
27463
+ throw new Error("drawBitmap size does not match the texture dimensions.");
27464
+ }
27465
+ this.drawBitmap = drawingBitmap;
27466
+ this.refreshDrawing();
27467
+ }
27468
+ }
27378
27469
  this.updateGLVolume();
27379
27470
  this.drawScene();
27380
27471
  this.onDocumentLoaded(document2);
@@ -27571,6 +27662,44 @@ var Niivue = class {
27571
27662
  }
27572
27663
  return this;
27573
27664
  }
27665
+ async loadDicoms(dicomList) {
27666
+ if (!this.getDicomLoader()) {
27667
+ throw new Error("No dicom loader set. Please set a dicom loader before loading dicoms");
27668
+ }
27669
+ this.drawScene();
27670
+ this.volumes = [];
27671
+ this.gl.clearColor(0, 0, 0, 1);
27672
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
27673
+ const promises = dicomList.map(async (dicom) => {
27674
+ let dicomData = null;
27675
+ if (dicom.isManifest) {
27676
+ dicomData = await NVImage.fetchDicomData(dicom.url);
27677
+ } else {
27678
+ const response = await fetch(dicom.url);
27679
+ if (!response.ok) {
27680
+ throw new Error(`Failed to load file: ${response.statusText}`);
27681
+ }
27682
+ const dicomArrayBuffer = await response.arrayBuffer();
27683
+ const basename = dicom.url.split("/").pop();
27684
+ const data2 = dicomArrayBuffer;
27685
+ dicomData = [{ name: basename, data: data2 }];
27686
+ }
27687
+ const dicomLoader = this.getDicomLoader().loader;
27688
+ const convertedArrayBuffer = await dicomLoader(dicomData);
27689
+ console.log(convertedArrayBuffer);
27690
+ const name = convertedArrayBuffer[0].name;
27691
+ const data = convertedArrayBuffer[0].data;
27692
+ const image = NVImage.loadFromUrl({ url: data, name });
27693
+ return image;
27694
+ });
27695
+ const nvImages = await Promise.all(promises);
27696
+ if (nvImages.length === 1) {
27697
+ this.addVolume(nvImages[0]);
27698
+ } else {
27699
+ this.onDicomLoaderFinishedWithImages(nvImages);
27700
+ }
27701
+ return this;
27702
+ }
27574
27703
  /**
27575
27704
  * load an array of volume objects
27576
27705
  * @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
@@ -29615,6 +29744,17 @@ var Niivue = class {
29615
29744
  mxNeg = Math.max(overlayItem.cal_minNeg, overlayItem.cal_maxNeg);
29616
29745
  }
29617
29746
  }
29747
+ if (layer > 0 && this.overlayOutlineWidth > 0) {
29748
+ const A = overlayItem.cal_min;
29749
+ const B = overlayItem.cal_max;
29750
+ let isZeroCrossing = Math.min(A, B) <= 0 && Math.max(A, B) >= 0;
29751
+ if (!isZeroCrossing && mnNeg < mxNeg) {
29752
+ isZeroCrossing = mnNeg <= 0 && mxNeg >= 0;
29753
+ }
29754
+ if (isZeroCrossing) {
29755
+ log.error("issue1139: do not use overlayOutlineWidth when thresholds cross or touch zero");
29756
+ }
29757
+ }
29618
29758
  if (!orientShader) {
29619
29759
  throw new Error("orientShader undefined");
29620
29760
  }