@niivue/niivue 0.52.0 → 0.53.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
@@ -14,10 +14,10 @@ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot
14
14
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
15
15
 
16
16
  // src/niivue/index.ts
17
- import { mat4 as mat46, vec2, vec3 as vec311, vec4 as vec48 } from "gl-matrix";
17
+ import { mat4 as mat49, vec2, vec3 as vec314, vec4 as vec49 } from "gl-matrix";
18
18
 
19
19
  // package.json
20
- var version = "0.52.0";
20
+ var version = "0.53.0";
21
21
 
22
22
  // src/logger.ts
23
23
  var _Log = class _Log {
@@ -1161,7 +1161,7 @@ void main(void) {
1161
1161
  //https://stackoverflow.com/questions/5879403/opengl-texture-coordinates-in-pixel-space
1162
1162
  float nlayer = float(textureSize(colormap, 0).y);
1163
1163
  //each volume has two color maps:
1164
- // (layer*2) = negative and (layer * 2) + 1 = postive
1164
+ // (layer*2) = negative and (layer * 2) + 1 = positive
1165
1165
  float y = ((2.0 * layer) + 1.5)/nlayer;
1166
1166
  FragColor = texture(colormap, vec2(txl, y)).rgba;
1167
1167
  //negative colors
@@ -3377,10 +3377,10 @@ __publicField(_NiivueObject3D, "makeCylinder", function(vertices, indices, start
3377
3377
  const idx0 = Math.floor(vertices.length / 3);
3378
3378
  const idx = new Uint32Array(num_f * 3);
3379
3379
  const vtx = new Float32Array(num_v * 3);
3380
- function setV(i, vec312) {
3381
- vtx[i * 3 + 0] = vec312[0];
3382
- vtx[i * 3 + 1] = vec312[1];
3383
- vtx[i * 3 + 2] = vec312[2];
3380
+ function setV(i, vec315) {
3381
+ vtx[i * 3 + 0] = vec315[0];
3382
+ vtx[i * 3 + 1] = vec315[1];
3383
+ vtx[i * 3 + 2] = vec315[2];
3384
3384
  }
3385
3385
  function setI(i, a, b, c) {
3386
3386
  idx[i * 3 + 0] = a + idx0;
@@ -3448,7 +3448,7 @@ __publicField(_NiivueObject3D, "makeColoredSphere", function(vertices, indices,
3448
3448
  var NiivueObject3D = _NiivueObject3D;
3449
3449
 
3450
3450
  // src/nvmesh.ts
3451
- import { vec3 as vec38, vec4 as vec46 } from "gl-matrix";
3451
+ import { vec3 as vec310, vec4 as vec47 } from "gl-matrix";
3452
3452
  import { v4 as uuidv42 } from "@lukeed/uuid";
3453
3453
 
3454
3454
  // src/cmaps/index.js
@@ -11984,7 +11984,7 @@ var NVUtilities = class _NVUtilities {
11984
11984
  const decompressed = await _NVUtilities.decompress(data);
11985
11985
  return decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
11986
11986
  }
11987
- static async readMatV4(buffer) {
11987
+ static async readMatV4(buffer, isReplaceDots = false) {
11988
11988
  let len = buffer.byteLength;
11989
11989
  if (len < 40) {
11990
11990
  throw new Error("File too small to be MAT v4: bytes = " + buffer.byteLength);
@@ -12040,7 +12040,10 @@ var NVUtilities = class _NVUtilities {
12040
12040
  throw new Error("mrows * ncols must be greater than one");
12041
12041
  }
12042
12042
  const byteArray = new Uint8Array(bytes.subarray(pos, pos + namlen));
12043
- const tagName = textDecoder.decode(byteArray).trim().replaceAll("\0", "");
12043
+ let tagName = textDecoder.decode(byteArray).trim().replaceAll("\0", "");
12044
+ if (isReplaceDots) {
12045
+ tagName = tagName.replaceAll(".", "_");
12046
+ }
12044
12047
  const tagDataType = getTensDigit(mtype);
12045
12048
  let tagBytesPerItem = 8;
12046
12049
  if (tagDataType >= 1 && tagDataType <= 2) {
@@ -15726,18 +15729,11 @@ var NVMeshLoaders = class _NVMeshLoaders {
15726
15729
 
15727
15730
  // src/nvdocument.ts
15728
15731
  import { serialize, deserialize } from "@ungap/structured-clone";
15729
- import { vec3 as vec37, vec4 as vec45 } from "gl-matrix";
15732
+ import { vec3 as vec39, vec4 as vec46 } from "gl-matrix";
15730
15733
 
15731
15734
  // src/nvimage/index.ts
15732
- import {
15733
- NIFTI1,
15734
- NIFTIEXTENSION,
15735
- isCompressed,
15736
- decompressAsync,
15737
- readImage,
15738
- readHeaderAsync
15739
- } from "nifti-reader-js";
15740
- import { mat3, mat4 as mat44, vec3 as vec36, vec4 as vec44 } from "gl-matrix";
15735
+ import { NIFTI1 as NIFTI14, NIFTIEXTENSION, readHeaderAsync as readHeaderAsync2 } from "nifti-reader-js";
15736
+ import { mat3 as mat32, mat4 as mat46, vec3 as vec38, vec4 as vec45 } from "gl-matrix";
15741
15737
  import { v4 as uuidv4 } from "@lukeed/uuid";
15742
15738
  import { Gunzip } from "fflate";
15743
15739
 
@@ -15769,6 +15765,9 @@ var ImageType = /* @__PURE__ */ ((ImageType3) => {
15769
15765
  ImageType3[ImageType3["SRC"] = 17] = "SRC";
15770
15766
  ImageType3[ImageType3["FIB"] = 18] = "FIB";
15771
15767
  ImageType3[ImageType3["BMP"] = 19] = "BMP";
15768
+ ImageType3[ImageType3["ZARR"] = 20] = "ZARR";
15769
+ ImageType3[ImageType3["NPY"] = 21] = "NPY";
15770
+ ImageType3[ImageType3["NPZ"] = 22] = "NPZ";
15772
15771
  return ImageType3;
15773
15772
  })(ImageType || {});
15774
15773
  var NVIMAGE_TYPE = Object.freeze({
@@ -15783,6 +15782,9 @@ var NVIMAGE_TYPE = Object.freeze({
15783
15782
  case "TXT":
15784
15783
  imageType = 3 /* DCM_MANIFEST */;
15785
15784
  break;
15785
+ case "FZ":
15786
+ case "GQI":
15787
+ case "QSDR":
15786
15788
  case "FIB":
15787
15789
  imageType = 18 /* FIB */;
15788
15790
  break;
@@ -15813,6 +15815,12 @@ var NVIMAGE_TYPE = Object.freeze({
15813
15815
  case "MGZ":
15814
15816
  imageType = 11 /* MGZ */;
15815
15817
  break;
15818
+ case "NPY":
15819
+ imageType = 21 /* NPY */;
15820
+ break;
15821
+ case "NPZ":
15822
+ imageType = 22 /* NPZ */;
15823
+ break;
15816
15824
  case "SRC":
15817
15825
  imageType = 17 /* SRC */;
15818
15826
  break;
@@ -15835,6 +15843,9 @@ var NVIMAGE_TYPE = Object.freeze({
15835
15843
  case "JPEG":
15836
15844
  imageType = 19 /* BMP */;
15837
15845
  break;
15846
+ case "ZARR":
15847
+ imageType = 20 /* ZARR */;
15848
+ break;
15838
15849
  }
15839
15850
  return imageType;
15840
15851
  }
@@ -16073,6 +16084,882 @@ async function uncompressStream(stream) {
16073
16084
  return uncompressedStream;
16074
16085
  }
16075
16086
 
16087
+ // src/nvimage/ImageWriter.ts
16088
+ import { NIFTI1 } from "nifti-reader-js";
16089
+ function createNiftiHeader(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */) {
16090
+ const hdr = new NIFTI1();
16091
+ hdr.littleEndian = true;
16092
+ hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
16093
+ hdr.dims[0] = Math.max(3, dims.length);
16094
+ for (let i = 0; i < dims.length; i++) {
16095
+ hdr.dims[i + 1] = dims[i];
16096
+ }
16097
+ hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
16098
+ for (let i = 0; i < dims.length; i++) {
16099
+ hdr.pixDims[i + 1] = pixDims[i];
16100
+ }
16101
+ if (affine.length === 16) {
16102
+ let k = 0;
16103
+ for (let i = 0; i < 4; i++) {
16104
+ for (let j = 0; j < 4; j++) {
16105
+ hdr.affine[i][j] = affine[k];
16106
+ k++;
16107
+ }
16108
+ }
16109
+ }
16110
+ let bpv = 8;
16111
+ if (datatypeCode === 256 /* DT_INT8 */ || datatypeCode === 2 /* DT_UINT8 */) {
16112
+ bpv = 8;
16113
+ } else if (datatypeCode === 512 /* DT_UINT16 */ || datatypeCode === 4 /* DT_INT16 */) {
16114
+ bpv = 16;
16115
+ } else if (datatypeCode === 16 /* DT_FLOAT32 */ || datatypeCode === 768 /* DT_UINT32 */ || datatypeCode === 8 /* DT_INT32 */ || datatypeCode === 2304 /* DT_RGBA32 */) {
16116
+ bpv = 32;
16117
+ } else if (datatypeCode === 64 /* DT_FLOAT64 */) {
16118
+ bpv = 64;
16119
+ } else {
16120
+ log.warn("Unsupported NIfTI datatypeCode for header creation: " + datatypeCode);
16121
+ }
16122
+ hdr.datatypeCode = datatypeCode;
16123
+ hdr.numBitsPerVoxel = bpv;
16124
+ hdr.scl_inter = 0;
16125
+ hdr.scl_slope = 1;
16126
+ hdr.sform_code = 2;
16127
+ hdr.magic = "n+1";
16128
+ hdr.vox_offset = 352;
16129
+ return hdr;
16130
+ }
16131
+ function createNiftiArray(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */, img = new Uint8Array()) {
16132
+ const hdr = createNiftiHeader(dims, pixDims, affine, datatypeCode);
16133
+ const hdrBytes = hdrToArrayBuffer(hdr, false);
16134
+ hdr.vox_offset = Math.max(352, hdrBytes.length);
16135
+ const finalHdrBytes = hdrToArrayBuffer(hdr, false);
16136
+ if (img.length < 1) {
16137
+ return finalHdrBytes;
16138
+ }
16139
+ const paddingSize = Math.max(0, hdr.vox_offset - finalHdrBytes.length);
16140
+ const padding = new Uint8Array(paddingSize);
16141
+ const imgBytes = new Uint8Array(img.buffer, img.byteOffset, img.byteLength);
16142
+ const totalLength = hdr.vox_offset + imgBytes.length;
16143
+ const outputData = new Uint8Array(totalLength);
16144
+ outputData.set(finalHdrBytes, 0);
16145
+ outputData.set(padding, finalHdrBytes.length);
16146
+ outputData.set(imgBytes, hdr.vox_offset);
16147
+ return outputData;
16148
+ }
16149
+ function toUint8Array(nvImage, drawingBytes = null) {
16150
+ if (!nvImage.hdr) {
16151
+ throw new Error("NVImage header is not defined for toUint8Array");
16152
+ }
16153
+ if (!nvImage.img && drawingBytes === null) {
16154
+ throw new Error("NVImage image data is not defined for toUint8Array");
16155
+ }
16156
+ const isDrawing = drawingBytes !== null;
16157
+ const hdrCopy = JSON.parse(JSON.stringify(nvImage.hdr));
16158
+ hdrCopy.vox_offset = Math.max(352, hdrCopy.vox_offset);
16159
+ if (isDrawing) {
16160
+ hdrCopy.datatypeCode = 2 /* DT_UINT8 */;
16161
+ hdrCopy.numBitsPerVoxel = 8;
16162
+ hdrCopy.scl_slope = 1;
16163
+ hdrCopy.scl_inter = 0;
16164
+ }
16165
+ const hdrBytes = hdrToArrayBuffer(hdrCopy, isDrawing);
16166
+ let imageBytesToSave;
16167
+ if (isDrawing) {
16168
+ const drawingBytesCurrent = drawingBytes;
16169
+ const perm = nvImage.permRAS;
16170
+ if (perm && (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3)) {
16171
+ log.debug("Reorienting drawing bytes back to native space for saving...");
16172
+ const dims = nvImage.hdr.dims;
16173
+ const nVox = dims[1] * dims[2] * dims[3];
16174
+ const nVoxRAS = nvImage.dimsRAS ? nvImage.dimsRAS[1] * nvImage.dimsRAS[2] * nvImage.dimsRAS[3] : nVox;
16175
+ if (drawingBytesCurrent.length !== nVoxRAS) {
16176
+ console.warn(
16177
+ `Drawing length (${drawingBytesCurrent.length}) does not match expected RAS voxel count (${nVoxRAS}). Cannot reorient drawing reliably.`
16178
+ );
16179
+ imageBytesToSave = drawingBytesCurrent;
16180
+ } else if (!nvImage.img2RASstep || !nvImage.img2RASstart || !nvImage.dimsRAS) {
16181
+ console.warn(
16182
+ `Missing RAS transformation info (img2RASstep, img2RASstart, dimsRAS). Cannot reorient drawing reliably.`
16183
+ );
16184
+ imageBytesToSave = drawingBytesCurrent;
16185
+ } else {
16186
+ const step = nvImage.img2RASstep;
16187
+ const start = nvImage.img2RASstart;
16188
+ const dimsRAS = nvImage.dimsRAS;
16189
+ const nativeData = new Uint8Array(nVox);
16190
+ nativeData.fill(0);
16191
+ const inputDrawingRAS = drawingBytesCurrent;
16192
+ let rasIndex = 0;
16193
+ for (let rz = 0; rz < dimsRAS[3]; rz++) {
16194
+ const zi = start[2] + rz * step[2];
16195
+ for (let ry = 0; ry < dimsRAS[2]; ry++) {
16196
+ const yi = start[1] + ry * step[1];
16197
+ for (let rx = 0; rx < dimsRAS[1]; rx++) {
16198
+ const xi = start[0] + rx * step[0];
16199
+ const nativeIndex = xi + yi + zi;
16200
+ if (nativeIndex >= 0 && nativeIndex < nVox) {
16201
+ nativeData[nativeIndex] = inputDrawingRAS[rasIndex];
16202
+ } else if (rasIndex < inputDrawingRAS.length) {
16203
+ console.warn(
16204
+ `Calculated native index ${nativeIndex} is out of bounds [0..${nVox - 1}] during drawing reorientation.`
16205
+ );
16206
+ }
16207
+ rasIndex++;
16208
+ }
16209
+ }
16210
+ }
16211
+ imageBytesToSave = nativeData;
16212
+ }
16213
+ } else {
16214
+ imageBytesToSave = drawingBytesCurrent;
16215
+ }
16216
+ } else {
16217
+ if (!nvImage.img) {
16218
+ throw new Error("NVImage image data is null when trying to save non-drawing.");
16219
+ }
16220
+ imageBytesToSave = new Uint8Array(nvImage.img.buffer, nvImage.img.byteOffset, nvImage.img.byteLength);
16221
+ }
16222
+ const headerSize = hdrBytes.length;
16223
+ const paddingSize = Math.max(0, hdrCopy.vox_offset - headerSize);
16224
+ const padding = new Uint8Array(paddingSize);
16225
+ const totalLength = hdrCopy.vox_offset + imageBytesToSave.length;
16226
+ const outputData = new Uint8Array(totalLength);
16227
+ outputData.set(hdrBytes, 0);
16228
+ outputData.set(padding, headerSize);
16229
+ outputData.set(imageBytesToSave, hdrCopy.vox_offset);
16230
+ return outputData;
16231
+ }
16232
+ async function saveToUint8Array(nvImage, fnm, drawing8 = null) {
16233
+ const odata = toUint8Array(nvImage, drawing8);
16234
+ const compress = fnm.toLowerCase().endsWith(".gz");
16235
+ if (compress) {
16236
+ try {
16237
+ const compressedData = await NVUtilities.compress(odata, "gzip");
16238
+ return new Uint8Array(compressedData);
16239
+ } catch (error) {
16240
+ log.error("Compression failed:", error);
16241
+ log.warn("Returning uncompressed data due to compression error.");
16242
+ return odata;
16243
+ }
16244
+ } else {
16245
+ return odata;
16246
+ }
16247
+ }
16248
+ async function saveToDisk(nvImage, fnm = "", drawing8 = null) {
16249
+ const saveData = await saveToUint8Array(nvImage, fnm, drawing8);
16250
+ if (!fnm) {
16251
+ log.debug("saveToDisk: empty file name, returning data as Uint8Array rather than triggering download");
16252
+ return saveData;
16253
+ }
16254
+ try {
16255
+ const blob = new Blob([saveData.buffer], {
16256
+ type: "application/octet-stream"
16257
+ // Standard type for binary download
16258
+ });
16259
+ const blobUrl = URL.createObjectURL(blob);
16260
+ const link = document.createElement("a");
16261
+ link.setAttribute("href", blobUrl);
16262
+ link.setAttribute("download", fnm);
16263
+ link.style.visibility = "hidden";
16264
+ document.body.appendChild(link);
16265
+ link.click();
16266
+ document.body.removeChild(link);
16267
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
16268
+ } catch (e) {
16269
+ log.error("Failed to trigger download:", e);
16270
+ }
16271
+ return saveData;
16272
+ }
16273
+
16274
+ // src/nvimage/VolumeUtils.ts
16275
+ function getValue(nvImage, x, y, z, frame4D = 0, isReadImaginary = false) {
16276
+ if (!nvImage.hdr) {
16277
+ throw new Error("getValue: NVImage header is not defined.");
16278
+ }
16279
+ if (!isReadImaginary && !nvImage.img) {
16280
+ throw new Error("getValue: NVImage image data is not defined.");
16281
+ }
16282
+ if (isReadImaginary && !nvImage.imaginary) {
16283
+ log.warn("getValue: Attempted to read imaginary data, but none exists.");
16284
+ return 0;
16285
+ }
16286
+ const nx = nvImage.hdr.dims[1];
16287
+ const ny = nvImage.hdr.dims[2];
16288
+ const nz = nvImage.hdr.dims[3];
16289
+ x = Math.max(0, Math.min(Math.round(x), nx - 1));
16290
+ y = Math.max(0, Math.min(Math.round(y), ny - 1));
16291
+ z = Math.max(0, Math.min(Math.round(z), nz - 1));
16292
+ frame4D = Math.max(0, frame4D);
16293
+ let vx = x + y * nx + z * nx * ny;
16294
+ if (nvImage.hdr.datatypeCode === 2304 /* DT_RGBA32 */) {
16295
+ if (!nvImage.img) {
16296
+ return 0;
16297
+ }
16298
+ vx *= 4;
16299
+ if (vx + 2 >= nvImage.img.length) {
16300
+ log.warn(`getValue: Calculated index ${vx} out of bounds for RGBA data.`);
16301
+ return 0;
16302
+ }
16303
+ const lum = nvImage.img[vx] * 0.2126 + nvImage.img[vx + 1] * 0.7152 + nvImage.img[vx + 2] * 0.0722;
16304
+ return Math.round(lum);
16305
+ }
16306
+ if (nvImage.hdr.datatypeCode === 128 /* DT_RGB24 */) {
16307
+ if (!nvImage.img) {
16308
+ return 0;
16309
+ }
16310
+ vx *= 3;
16311
+ if (vx + 2 >= nvImage.img.length) {
16312
+ log.warn(`getValue: Calculated index ${vx} out of bounds for RGB data.`);
16313
+ return 0;
16314
+ }
16315
+ const lum = nvImage.img[vx] * 0.2126 + nvImage.img[vx + 1] * 0.7152 + nvImage.img[vx + 2] * 0.0722;
16316
+ return Math.round(lum);
16317
+ }
16318
+ const nVox3D = nx * ny * nz;
16319
+ const volOffset = frame4D * nVox3D;
16320
+ const finalVxIndex = vx + volOffset;
16321
+ const dataArray = isReadImaginary ? nvImage.imaginary : nvImage.img;
16322
+ if (finalVxIndex < 0 || finalVxIndex >= dataArray.length) {
16323
+ return 0;
16324
+ }
16325
+ const rawValue = dataArray[finalVxIndex];
16326
+ const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1 : nvImage.hdr.scl_slope;
16327
+ const inter = isNaN(nvImage.hdr.scl_inter) ? 0 : nvImage.hdr.scl_inter;
16328
+ return slope * rawValue + inter;
16329
+ }
16330
+ function getVolumeData(nvImage, voxStartRAS = [-1, 0, 0], voxEndRAS = [0, 0, 0], dataType = "same") {
16331
+ const defaultResult = [new Uint8Array(), [0, 0, 0]];
16332
+ if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {
16333
+ log.error("getVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).");
16334
+ return defaultResult;
16335
+ }
16336
+ voxStartRAS = voxStartRAS.slice(0, 3);
16337
+ voxEndRAS = voxEndRAS.slice(0, 3);
16338
+ if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {
16339
+ log.warn("getVolumeData: Invalid start or end coordinates provided.");
16340
+ return defaultResult;
16341
+ }
16342
+ const dimsRAS = nvImage.dimsRAS.slice(1, 4);
16343
+ for (let i = 0; i < 3; i++) {
16344
+ voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1));
16345
+ voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1));
16346
+ if (voxEndRAS[i] < voxStartRAS[i]) {
16347
+ const tmp = voxEndRAS[i];
16348
+ voxEndRAS[i] = voxStartRAS[i];
16349
+ voxStartRAS[i] = tmp;
16350
+ }
16351
+ }
16352
+ const slabDims = [
16353
+ voxEndRAS[0] - voxStartRAS[0] + 1,
16354
+ voxEndRAS[1] - voxStartRAS[1] + 1,
16355
+ voxEndRAS[2] - voxStartRAS[2] + 1
16356
+ ];
16357
+ const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
16358
+ if (slabNVox <= 0) {
16359
+ log.warn("getVolumeData: Calculated slab size is zero or negative.");
16360
+ return defaultResult;
16361
+ }
16362
+ let OutputArrayConstructor = nvImage.img.constructor;
16363
+ if (dataType === "uint8") {
16364
+ OutputArrayConstructor = Uint8Array;
16365
+ } else if (dataType === "int16") {
16366
+ OutputArrayConstructor = Int16Array;
16367
+ } else if (dataType === "uint16") {
16368
+ OutputArrayConstructor = Uint16Array;
16369
+ } else if (dataType === "float32" || dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
16370
+ OutputArrayConstructor = Float32Array;
16371
+ } else if (dataType === "float64") {
16372
+ OutputArrayConstructor = Float64Array;
16373
+ } else if (dataType !== "same") {
16374
+ log.warn(`getVolumeData: Unsupported dataType '${dataType}'. Using 'same'.`);
16375
+ }
16376
+ let outputImg;
16377
+ try {
16378
+ outputImg = new OutputArrayConstructor(slabNVox);
16379
+ } catch (e) {
16380
+ log.error(`getVolumeData: Failed to create output array for dataType '${dataType}'.`, e);
16381
+ return defaultResult;
16382
+ }
16383
+ const step = nvImage.img2RASstep;
16384
+ const start = nvImage.img2RASstart;
16385
+ const sourceImg = nvImage.img;
16386
+ let outputIndex = 0;
16387
+ for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {
16388
+ const zi = start[2] + rz * step[2];
16389
+ for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {
16390
+ const yi = start[1] + ry * step[1];
16391
+ for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {
16392
+ const xi = start[0] + rx * step[0];
16393
+ const nativeIndex = xi + yi + zi;
16394
+ let value = 0;
16395
+ if (nativeIndex >= 0 && nativeIndex < sourceImg.length) {
16396
+ value = sourceImg[nativeIndex];
16397
+ } else {
16398
+ }
16399
+ outputImg[outputIndex++] = value;
16400
+ }
16401
+ }
16402
+ }
16403
+ const slope = isNaN(nvImage.hdr.scl_slope) || nvImage.hdr.scl_slope === 0 ? 1 : nvImage.hdr.scl_slope;
16404
+ const inter = isNaN(nvImage.hdr.scl_inter) ? 0 : nvImage.hdr.scl_inter;
16405
+ if (dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
16406
+ if (!(outputImg instanceof Float32Array)) {
16407
+ log.warn(`getVolumeData: Converting output to Float32 for scaling type '${dataType}'.`);
16408
+ outputImg = Float32Array.from(outputImg);
16409
+ }
16410
+ for (let i = 0; i < outputImg.length; i++) {
16411
+ outputImg[i] = outputImg[i] * slope + inter;
16412
+ }
16413
+ }
16414
+ if (dataType === "normalized" || dataType === "windowed") {
16415
+ let minVal = nvImage.cal_min;
16416
+ let maxVal = nvImage.cal_max;
16417
+ if (dataType === "normalized") {
16418
+ minVal = nvImage.global_min;
16419
+ maxVal = nvImage.global_max;
16420
+ }
16421
+ const range = maxVal - minVal;
16422
+ const scale = range === 0 ? 0 : 1 / range;
16423
+ for (let i = 0; i < outputImg.length; i++) {
16424
+ outputImg[i] = (outputImg[i] - minVal) * scale;
16425
+ outputImg[i] = Math.max(0, Math.min(outputImg[i], 1));
16426
+ }
16427
+ }
16428
+ return [outputImg, slabDims];
16429
+ }
16430
+ function setVolumeData(nvImage, voxStartRAS = [-1, 0, 0], voxEndRAS = [0, 0, 0], slabData = new Uint8Array()) {
16431
+ if (!nvImage.hdr || !nvImage.img || !nvImage.dimsRAS || !nvImage.img2RASstep || !nvImage.img2RASstart) {
16432
+ log.error("setVolumeData: Missing required NVImage properties (hdr, img, dimsRAS, img2RASstep/start).");
16433
+ return;
16434
+ }
16435
+ if (slabData.length < 1) {
16436
+ log.warn("setVolumeData: Input slabData is empty.");
16437
+ return;
16438
+ }
16439
+ voxStartRAS = voxStartRAS.slice(0, 3);
16440
+ voxEndRAS = voxEndRAS.slice(0, 3);
16441
+ if (Math.min(...voxStartRAS) < 0 || Math.min(...voxEndRAS) < 0) {
16442
+ log.warn("setVolumeData: Invalid start or end coordinates provided.");
16443
+ return;
16444
+ }
16445
+ const dimsRAS = nvImage.dimsRAS.slice(1, 4);
16446
+ for (let i = 0; i < 3; i++) {
16447
+ voxStartRAS[i] = Math.max(0, Math.min(Math.round(voxStartRAS[i]), dimsRAS[i] - 1));
16448
+ voxEndRAS[i] = Math.max(0, Math.min(Math.round(voxEndRAS[i]), dimsRAS[i] - 1));
16449
+ if (voxEndRAS[i] < voxStartRAS[i]) {
16450
+ const tmp = voxEndRAS[i];
16451
+ voxEndRAS[i] = voxStartRAS[i];
16452
+ voxStartRAS[i] = tmp;
16453
+ }
16454
+ }
16455
+ const slabDims = [
16456
+ voxEndRAS[0] - voxStartRAS[0] + 1,
16457
+ voxEndRAS[1] - voxStartRAS[1] + 1,
16458
+ voxEndRAS[2] - voxStartRAS[2] + 1
16459
+ ];
16460
+ const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
16461
+ if (slabNVox <= 0) {
16462
+ log.warn("setVolumeData: Calculated slab size is zero or negative.");
16463
+ return;
16464
+ }
16465
+ if (slabData.length < slabNVox) {
16466
+ log.error(
16467
+ `setVolumeData: Input slabData length (${slabData.length}) is less than the calculated slab size (${slabNVox}).`
16468
+ );
16469
+ return;
16470
+ }
16471
+ const step = nvImage.img2RASstep;
16472
+ const start = nvImage.img2RASstart;
16473
+ const targetImg = nvImage.img;
16474
+ let sourceIndex = 0;
16475
+ for (let rz = voxStartRAS[2]; rz <= voxEndRAS[2]; rz++) {
16476
+ const zi = start[2] + rz * step[2];
16477
+ for (let ry = voxStartRAS[1]; ry <= voxEndRAS[1]; ry++) {
16478
+ const yi = start[1] + ry * step[1];
16479
+ for (let rx = voxStartRAS[0]; rx <= voxEndRAS[0]; rx++) {
16480
+ const xi = start[0] + rx * step[0];
16481
+ const nativeIndex = xi + yi + zi;
16482
+ if (nativeIndex >= 0 && nativeIndex < targetImg.length) {
16483
+ targetImg[nativeIndex] = slabData[sourceIndex];
16484
+ } else {
16485
+ }
16486
+ sourceIndex++;
16487
+ }
16488
+ }
16489
+ }
16490
+ }
16491
+
16492
+ // src/nvimage/ImageReaders/mgh.ts
16493
+ var mgh_exports = {};
16494
+ __export(mgh_exports, {
16495
+ readMgh: () => readMgh
16496
+ });
16497
+ import { NIFTI1 as NIFTI12 } from "nifti-reader-js";
16498
+ import { mat4 as mat44, vec4 as vec44, vec3 as vec36 } from "gl-matrix";
16499
+ async function readMgh(nvImage, buffer) {
16500
+ if (!nvImage.hdr) {
16501
+ log.warn("readMgh called before nvImage.hdr was initialized. Creating default.");
16502
+ nvImage.hdr = new NIFTI12();
16503
+ }
16504
+ const hdr = nvImage.hdr;
16505
+ hdr.littleEndian = false;
16506
+ let raw = buffer;
16507
+ let reader = new DataView(raw);
16508
+ if (raw.byteLength >= 2 && reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {
16509
+ try {
16510
+ raw = await NVUtilities.decompressToBuffer(new Uint8Array(buffer));
16511
+ reader = new DataView(raw);
16512
+ } catch (err) {
16513
+ log.error("Failed to decompress MGZ file.", err);
16514
+ return null;
16515
+ }
16516
+ }
16517
+ if (raw.byteLength < 284) {
16518
+ log.error("File too small to be a valid MGH/MGZ header.");
16519
+ return null;
16520
+ }
16521
+ const version2 = reader.getInt32(0, false);
16522
+ const width = reader.getInt32(4, false);
16523
+ const height = reader.getInt32(8, false);
16524
+ const depth = reader.getInt32(12, false);
16525
+ const nframes = reader.getInt32(16, false);
16526
+ const mtype = reader.getInt32(20, false);
16527
+ const spacingX = reader.getFloat32(30, false);
16528
+ const spacingY = reader.getFloat32(34, false);
16529
+ const spacingZ = reader.getFloat32(38, false);
16530
+ const xr = reader.getFloat32(42, false);
16531
+ const xa = reader.getFloat32(46, false);
16532
+ const xs = reader.getFloat32(50, false);
16533
+ const yr = reader.getFloat32(54, false);
16534
+ const ya = reader.getFloat32(58, false);
16535
+ const ys = reader.getFloat32(62, false);
16536
+ const zr = reader.getFloat32(66, false);
16537
+ const za = reader.getFloat32(70, false);
16538
+ const zs = reader.getFloat32(74, false);
16539
+ const cr = reader.getFloat32(78, false);
16540
+ const ca = reader.getFloat32(82, false);
16541
+ const cs = reader.getFloat32(86, false);
16542
+ if (version2 !== 1) {
16543
+ log.warn(`Unexpected MGH version: ${version2}.`);
16544
+ }
16545
+ if (width <= 0 || height <= 0 || depth <= 0) {
16546
+ log.error(`Invalid MGH dimensions: ${width}x${height}x${depth}`);
16547
+ return null;
16548
+ }
16549
+ switch (mtype) {
16550
+ case 0:
16551
+ hdr.numBitsPerVoxel = 8;
16552
+ hdr.datatypeCode = 2 /* DT_UINT8 */;
16553
+ break;
16554
+ case 4:
16555
+ hdr.numBitsPerVoxel = 16;
16556
+ hdr.datatypeCode = 4 /* DT_INT16 */;
16557
+ break;
16558
+ case 1:
16559
+ hdr.numBitsPerVoxel = 32;
16560
+ hdr.datatypeCode = 8 /* DT_INT32 */;
16561
+ break;
16562
+ case 3:
16563
+ hdr.numBitsPerVoxel = 32;
16564
+ hdr.datatypeCode = 16 /* DT_FLOAT32 */;
16565
+ break;
16566
+ default:
16567
+ log.error(`Unsupported MGH data type: ${mtype}`);
16568
+ return null;
16569
+ }
16570
+ hdr.dims[1] = width;
16571
+ hdr.dims[2] = height;
16572
+ hdr.dims[3] = depth;
16573
+ hdr.dims[4] = Math.max(1, nframes);
16574
+ hdr.dims[0] = hdr.dims[4] > 1 ? 4 : 3;
16575
+ hdr.pixDims[1] = Math.abs(spacingX);
16576
+ hdr.pixDims[2] = Math.abs(spacingY);
16577
+ hdr.pixDims[3] = Math.abs(spacingZ);
16578
+ hdr.pixDims[4] = 0;
16579
+ hdr.sform_code = 1;
16580
+ hdr.qform_code = 0;
16581
+ const rot44 = mat44.fromValues(
16582
+ xr * hdr.pixDims[1],
16583
+ yr * hdr.pixDims[2],
16584
+ zr * hdr.pixDims[3],
16585
+ 0,
16586
+ xa * hdr.pixDims[1],
16587
+ ya * hdr.pixDims[2],
16588
+ za * hdr.pixDims[3],
16589
+ 0,
16590
+ xs * hdr.pixDims[1],
16591
+ ys * hdr.pixDims[2],
16592
+ zs * hdr.pixDims[3],
16593
+ 0,
16594
+ 0,
16595
+ 0,
16596
+ 0,
16597
+ 1
16598
+ );
16599
+ const PcrsVec = vec44.fromValues(hdr.dims[1] / 2, hdr.dims[2] / 2, hdr.dims[3] / 2, 1);
16600
+ const PxyzOffsetVec = vec44.create();
16601
+ vec44.transformMat4(PxyzOffsetVec, PcrsVec, rot44);
16602
+ const translation = vec36.fromValues(cr - PxyzOffsetVec[0], ca - PxyzOffsetVec[1], cs - PxyzOffsetVec[2]);
16603
+ hdr.affine = [
16604
+ [rot44[0], rot44[1], rot44[2], translation[0]],
16605
+ [rot44[4], rot44[5], rot44[6], translation[1]],
16606
+ [rot44[8], rot44[9], rot44[10], translation[2]],
16607
+ [0, 0, 0, 1]
16608
+ ];
16609
+ hdr.vox_offset = 284;
16610
+ hdr.magic = "n+1";
16611
+ const nBytesPerVoxel = hdr.numBitsPerVoxel / 8;
16612
+ const nVoxels = width * height * depth * hdr.dims[4];
16613
+ const expectedBytes = nVoxels * nBytesPerVoxel;
16614
+ const remainingBytes = raw.byteLength - hdr.vox_offset;
16615
+ if (remainingBytes < expectedBytes) {
16616
+ log.error(`MGH image data size mismatch: expected ${expectedBytes}, found ${remainingBytes}`);
16617
+ return null;
16618
+ } else if (remainingBytes > expectedBytes) {
16619
+ log.warn(`MGH file has extra ${remainingBytes - expectedBytes} bytes after image data. Truncating.`);
16620
+ }
16621
+ const imgRaw = raw.slice(hdr.vox_offset, hdr.vox_offset + expectedBytes);
16622
+ return imgRaw;
16623
+ }
16624
+
16625
+ // src/nvimage/ImageReaders/nii.ts
16626
+ var nii_exports = {};
16627
+ __export(nii_exports, {
16628
+ readNifti: () => readNifti
16629
+ });
16630
+ import { isCompressed, decompressAsync, readHeaderAsync, readImage } from "nifti-reader-js";
16631
+ async function readNifti(nvImage, buffer) {
16632
+ let dataBuffer = buffer;
16633
+ let imgRaw = null;
16634
+ try {
16635
+ if (isCompressed(dataBuffer)) {
16636
+ log.debug(`Decompressing NIfTI file: ${nvImage.name}`);
16637
+ dataBuffer = await decompressAsync(dataBuffer);
16638
+ log.debug(`Decompression complete for: ${nvImage.name}`);
16639
+ }
16640
+ if (!dataBuffer || dataBuffer.byteLength === 0) {
16641
+ throw new Error("Buffer became invalid after decompression attempt.");
16642
+ }
16643
+ nvImage.hdr = await readHeaderAsync(dataBuffer);
16644
+ if (nvImage.hdr === null) {
16645
+ throw new Error(`Failed to read NIfTI header: ${nvImage.name}`);
16646
+ }
16647
+ if (nvImage.hdr.cal_min === 0 && nvImage.hdr.cal_max === 255 && nvImage.hdr.datatypeCode !== 2 /* DT_UINT8 */) {
16648
+ log.debug(`Resetting suspicious cal_min/max (0/255) for non-uint8 NIfTI: ${nvImage.name}`);
16649
+ nvImage.hdr.cal_min = 0;
16650
+ nvImage.hdr.cal_max = 0;
16651
+ }
16652
+ imgRaw = readImage(nvImage.hdr, dataBuffer);
16653
+ if (imgRaw === null) {
16654
+ throw new Error(`nifti-reader-js readImage returned null for ${nvImage.name}`);
16655
+ }
16656
+ return imgRaw;
16657
+ } catch (err) {
16658
+ log.error(`Error processing NIfTI file ${nvImage.name}:`, err);
16659
+ nvImage.hdr = null;
16660
+ return null;
16661
+ }
16662
+ }
16663
+
16664
+ // src/nvimage/ImageReaders/nrrd.ts
16665
+ var nrrd_exports = {};
16666
+ __export(nrrd_exports, {
16667
+ readNrrd: () => readNrrd
16668
+ });
16669
+ import { NIFTI1 as NIFTI13 } from "nifti-reader-js";
16670
+ import { mat3, mat4 as mat45, vec3 as vec37 } from "gl-matrix";
16671
+ async function readNrrd(nvImage, dataBuffer, pairedImgData = null) {
16672
+ if (!nvImage.hdr) {
16673
+ log.warn("readNrrd called before nvImage.hdr was initialized. Creating default.");
16674
+ nvImage.hdr = new NIFTI13();
16675
+ }
16676
+ const hdr = nvImage.hdr;
16677
+ hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
16678
+ const len = dataBuffer.byteLength;
16679
+ let txt = null;
16680
+ const bytes = new Uint8Array(dataBuffer);
16681
+ for (let i = 1; i < len; i++) {
16682
+ if (bytes[i - 1] === 10 && bytes[i] === 10) {
16683
+ const v = dataBuffer.slice(0, i - 1);
16684
+ txt = new TextDecoder().decode(v);
16685
+ hdr.vox_offset = i + 1;
16686
+ break;
16687
+ }
16688
+ }
16689
+ if (txt === null) {
16690
+ log.error("readNrrd: could not extract txt");
16691
+ return null;
16692
+ }
16693
+ const lines = txt.split("\n");
16694
+ if (!lines[0].startsWith("NRRD")) {
16695
+ log.error("Invalid NRRD image (magic signature missing)");
16696
+ return null;
16697
+ }
16698
+ const n = lines.length;
16699
+ let isGz = false;
16700
+ let isMicron = false;
16701
+ let isDetached = false;
16702
+ const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
16703
+ const offset = vec37.fromValues(0, 0, 0);
16704
+ let rot33 = mat3.create();
16705
+ for (let i = 1; i < n; i++) {
16706
+ let str = lines[i];
16707
+ if (str.length === 0 || str[0] === "#") {
16708
+ if (str.startsWith("#")) {
16709
+ continue;
16710
+ }
16711
+ if (str.trim().length === 0) {
16712
+ continue;
16713
+ }
16714
+ }
16715
+ str = str.toLowerCase();
16716
+ const items = str.split(":");
16717
+ if (items.length < 2) {
16718
+ continue;
16719
+ }
16720
+ const key = items[0].trim();
16721
+ let value = items[1].trim();
16722
+ value = value.replaceAll(")", " ");
16723
+ value = value.replaceAll("(", " ");
16724
+ value = value.trim();
16725
+ switch (key) {
16726
+ case "data file":
16727
+ isDetached = true;
16728
+ break;
16729
+ case "encoding":
16730
+ if (value.includes("raw")) {
16731
+ isGz = false;
16732
+ } else if (value.includes("gz")) {
16733
+ isGz = true;
16734
+ } else {
16735
+ log.error("Unsupported NRRD encoding");
16736
+ return null;
16737
+ }
16738
+ break;
16739
+ case "type":
16740
+ switch (value) {
16741
+ case "uchar":
16742
+ case "unsigned char":
16743
+ case "uint8":
16744
+ case "uint8_t":
16745
+ hdr.numBitsPerVoxel = 8;
16746
+ hdr.datatypeCode = 2 /* DT_UINT8 */;
16747
+ break;
16748
+ case "signed char":
16749
+ case "int8":
16750
+ case "int8_t":
16751
+ hdr.numBitsPerVoxel = 8;
16752
+ hdr.datatypeCode = 256 /* DT_INT8 */;
16753
+ break;
16754
+ case "short":
16755
+ case "short int":
16756
+ case "signed short":
16757
+ case "signed short int":
16758
+ case "int16":
16759
+ case "int16_t":
16760
+ hdr.numBitsPerVoxel = 16;
16761
+ hdr.datatypeCode = 4 /* DT_INT16 */;
16762
+ break;
16763
+ case "ushort":
16764
+ case "unsigned short":
16765
+ case "unsigned short int":
16766
+ case "uint16":
16767
+ case "uint16_t":
16768
+ hdr.numBitsPerVoxel = 16;
16769
+ hdr.datatypeCode = 512 /* DT_UINT16 */;
16770
+ break;
16771
+ case "int":
16772
+ case "signed int":
16773
+ case "int32":
16774
+ case "int32_t":
16775
+ hdr.numBitsPerVoxel = 32;
16776
+ hdr.datatypeCode = 8 /* DT_INT32 */;
16777
+ break;
16778
+ case "uint":
16779
+ case "unsigned int":
16780
+ case "uint32":
16781
+ case "uint32_t":
16782
+ hdr.numBitsPerVoxel = 32;
16783
+ hdr.datatypeCode = 768 /* DT_UINT32 */;
16784
+ break;
16785
+ case "float":
16786
+ hdr.numBitsPerVoxel = 32;
16787
+ hdr.datatypeCode = 16 /* DT_FLOAT32 */;
16788
+ break;
16789
+ case "double":
16790
+ hdr.numBitsPerVoxel = 64;
16791
+ hdr.datatypeCode = 64 /* DT_FLOAT64 */;
16792
+ break;
16793
+ default:
16794
+ log.error("Unsupported NRRD data type: " + value);
16795
+ return null;
16796
+ }
16797
+ break;
16798
+ case "spacings":
16799
+ {
16800
+ const values = value.split(/[ ,]+/);
16801
+ for (let d = 0; d < values.length; d++) {
16802
+ hdr.pixDims[d + 1] = parseFloat(values[d]);
16803
+ }
16804
+ }
16805
+ break;
16806
+ case "sizes":
16807
+ {
16808
+ const dims = value.split(/[ ,]+/);
16809
+ hdr.dims[0] = dims.length;
16810
+ for (let d = 0; d < dims.length; d++) {
16811
+ hdr.dims[d + 1] = parseInt(dims[d]);
16812
+ }
16813
+ }
16814
+ break;
16815
+ case "endian":
16816
+ if (value.includes("little")) {
16817
+ hdr.littleEndian = true;
16818
+ } else if (value.includes("big")) {
16819
+ hdr.littleEndian = false;
16820
+ }
16821
+ break;
16822
+ case "space directions":
16823
+ {
16824
+ const vs = value.split(/[ ,]+/);
16825
+ if (vs.length === 9) {
16826
+ for (let d = 0; d < 9; d++) {
16827
+ mat33[d] = parseFloat(vs[d]);
16828
+ }
16829
+ }
16830
+ }
16831
+ break;
16832
+ case "space origin":
16833
+ {
16834
+ const ts = value.split(/[ ,]+/);
16835
+ if (ts.length === 3) {
16836
+ offset[0] = parseFloat(ts[0]);
16837
+ offset[1] = parseFloat(ts[1]);
16838
+ offset[2] = parseFloat(ts[2]);
16839
+ }
16840
+ }
16841
+ break;
16842
+ case "space units":
16843
+ if (value.includes("microns")) {
16844
+ isMicron = true;
16845
+ }
16846
+ break;
16847
+ case "space":
16848
+ if (value.includes("right-anterior-superior") || value.includes("ras")) {
16849
+ rot33 = mat3.fromValues(1, 0, 0, 0, 1, 0, 0, 0, 1);
16850
+ } else if (value.includes("left-anterior-superior") || value.includes("las")) {
16851
+ rot33 = mat3.fromValues(-1, 0, 0, 0, 1, 0, 0, 0, 1);
16852
+ } else if (value.includes("left-posterior-superior") || value.includes("lps")) {
16853
+ rot33 = mat3.fromValues(-1, 0, 0, 0, -1, 0, 0, 0, 1);
16854
+ } else {
16855
+ log.warn("Unsupported NRRD space value:", value);
16856
+ }
16857
+ break;
16858
+ default:
16859
+ log.warn("Unknown:", key);
16860
+ break;
16861
+ }
16862
+ }
16863
+ if (!isNaN(mat33[0])) {
16864
+ hdr.sform_code = 2;
16865
+ if (isMicron) {
16866
+ mat45.multiplyScalar(mat33, mat33, 1e-3);
16867
+ offset[0] *= 1e-3;
16868
+ offset[1] *= 1e-3;
16869
+ offset[2] *= 1e-3;
16870
+ }
16871
+ if (rot33[0] < 0) {
16872
+ offset[0] = -offset[0];
16873
+ }
16874
+ if (rot33[4] < 0) {
16875
+ offset[1] = -offset[1];
16876
+ }
16877
+ if (rot33[8] < 0) {
16878
+ offset[2] = -offset[2];
16879
+ }
16880
+ mat3.multiply(mat33, rot33, mat33);
16881
+ const mat = mat45.fromValues(
16882
+ mat33[0],
16883
+ mat33[3],
16884
+ mat33[6],
16885
+ offset[0],
16886
+ mat33[1],
16887
+ mat33[4],
16888
+ mat33[7],
16889
+ offset[1],
16890
+ mat33[2],
16891
+ mat33[5],
16892
+ mat33[8],
16893
+ offset[2],
16894
+ 0,
16895
+ 0,
16896
+ 0,
16897
+ 1
16898
+ );
16899
+ if (!nvImage.vox2mm) {
16900
+ return null;
16901
+ }
16902
+ const mm000 = nvImage.vox2mm([0, 0, 0], mat);
16903
+ const mm100 = nvImage.vox2mm([1, 0, 0], mat);
16904
+ vec37.subtract(mm100, mm100, mm000);
16905
+ const mm010 = nvImage.vox2mm([0, 1, 0], mat);
16906
+ vec37.subtract(mm010, mm010, mm000);
16907
+ const mm001 = nvImage.vox2mm([0, 0, 1], mat);
16908
+ vec37.subtract(mm001, mm001, mm000);
16909
+ hdr.pixDims[1] = vec37.length(mm100);
16910
+ hdr.pixDims[2] = vec37.length(mm010);
16911
+ hdr.pixDims[3] = vec37.length(mm001);
16912
+ hdr.affine = [
16913
+ [mat[0], mat[1], mat[2], mat[3]],
16914
+ [mat[4], mat[5], mat[6], mat[7]],
16915
+ [mat[8], mat[9], mat[10], mat[11]],
16916
+ [0, 0, 0, 1]
16917
+ ];
16918
+ }
16919
+ let imgRaw = null;
16920
+ const sourceBuffer = isDetached ? pairedImgData : dataBuffer;
16921
+ const sourceOffset = isDetached ? 0 : hdr.vox_offset;
16922
+ if (isDetached && !sourceBuffer) {
16923
+ log.warn("Missing data: NRRD header describes detached data file but only one URL provided");
16924
+ return null;
16925
+ }
16926
+ if (!sourceBuffer || sourceOffset >= sourceBuffer.byteLength) {
16927
+ log.error(`NRRD data offset (${sourceOffset}) invalid for buffer length (${sourceBuffer?.byteLength ?? 0})`);
16928
+ return null;
16929
+ }
16930
+ let dataSection = sourceBuffer.slice(sourceOffset);
16931
+ if (isGz) {
16932
+ try {
16933
+ log.debug("Decompressing NRRD data...");
16934
+ dataSection = await NVUtilities.decompressToBuffer(new Uint8Array(dataSection));
16935
+ log.debug("Decompression complete.");
16936
+ } catch (err) {
16937
+ log.error("Failed to decompress NRRD data.", err);
16938
+ return null;
16939
+ }
16940
+ }
16941
+ const nBytesPerVoxel = hdr.numBitsPerVoxel / 8;
16942
+ const nVoxels = hdr.dims.slice(1, hdr.dims[0] + 1).reduce((acc, dim) => acc * Math.max(1, dim), 1);
16943
+ const expectedBytes = nVoxels * nBytesPerVoxel;
16944
+ if (dataSection.byteLength < expectedBytes) {
16945
+ log.error(`NRRD image data size mismatch: expected ${expectedBytes}, found ${dataSection.byteLength}`);
16946
+ return null;
16947
+ } else if (dataSection.byteLength > expectedBytes) {
16948
+ log.warn(`NRRD has extra ${dataSection.byteLength - expectedBytes} bytes after expected image data. Truncating.`);
16949
+ dataSection = dataSection.slice(0, expectedBytes);
16950
+ }
16951
+ imgRaw = dataSection;
16952
+ if (!hdr.datatypeCode) {
16953
+ log.error("NRRD parsing failed to set datatypeCode.");
16954
+ return null;
16955
+ }
16956
+ if (!hdr.numBitsPerVoxel) {
16957
+ log.error("NRRD parsing failed to set numBitsPerVoxel.");
16958
+ return null;
16959
+ }
16960
+ return imgRaw;
16961
+ }
16962
+
16076
16963
  // src/nvimage/index.ts
16077
16964
  var NVImage = class _NVImage {
16078
16965
  /**
@@ -16248,7 +17135,7 @@ var NVImage = class _NVImage {
16248
17135
  this.nFrame4D = nVol4D;
16249
17136
  }
16250
17137
  if ((this.hdr.intent_code === 1007 /* NIFTI_INTENT_VECTOR */ || this.hdr.intent_code === 2003 /* NIFTI_INTENT_RGB_VECTOR */) && this.nFrame4D === 3 && this.hdr.datatypeCode === 16 /* DT_FLOAT32 */) {
16251
- imgRaw = this.float32V1asRGBA(new Float32Array(imgRaw));
17138
+ imgRaw = this.float32V1asRGBA(new Float32Array(imgRaw)).buffer;
16252
17139
  }
16253
17140
  if (this.hdr.pixDims[1] === 0 || this.hdr.pixDims[2] === 0 || this.hdr.pixDims[3] === 0) {
16254
17141
  log.error("pixDims not plausible", this.hdr);
@@ -16486,7 +17373,10 @@ var NVImage = class _NVImage {
16486
17373
  break;
16487
17374
  case NVIMAGE_TYPE.NHDR:
16488
17375
  case NVIMAGE_TYPE.NRRD:
16489
- imgRaw = await newImg.readNRRD(dataBuffer, pairedImgData);
17376
+ imgRaw = await nrrd_exports.readNrrd(newImg, dataBuffer);
17377
+ if (imgRaw === null) {
17378
+ throw new Error(`Failed to parse NHDR/NRRD file ${name}`);
17379
+ }
16490
17380
  break;
16491
17381
  case NVIMAGE_TYPE.MHD:
16492
17382
  case NVIMAGE_TYPE.MHA:
@@ -16494,7 +17384,10 @@ var NVImage = class _NVImage {
16494
17384
  break;
16495
17385
  case NVIMAGE_TYPE.MGH:
16496
17386
  case NVIMAGE_TYPE.MGZ:
16497
- imgRaw = await newImg.readMGH(dataBuffer);
17387
+ imgRaw = await mgh_exports.readMgh(newImg, dataBuffer);
17388
+ if (imgRaw === null) {
17389
+ throw new Error(`Failed to parse MGH/MGZ file ${name}`);
17390
+ }
16498
17391
  break;
16499
17392
  case NVIMAGE_TYPE.SRC:
16500
17393
  imgRaw = await newImg.readSRC(dataBuffer);
@@ -16514,20 +17407,18 @@ var NVImage = class _NVImage {
16514
17407
  case NVIMAGE_TYPE.BMP:
16515
17408
  imgRaw = await newImg.readBMP(dataBuffer);
16516
17409
  break;
17410
+ case NVIMAGE_TYPE.NPY:
17411
+ imgRaw = await newImg.readNPY(dataBuffer);
17412
+ break;
17413
+ case NVIMAGE_TYPE.NPZ:
17414
+ imgRaw = await newImg.readNPZ(dataBuffer);
17415
+ break;
17416
+ case NVIMAGE_TYPE.ZARR:
17417
+ throw new Error("Image type ZARR not (yet) supported");
16517
17418
  case NVIMAGE_TYPE.NII:
16518
- if (isCompressed(dataBuffer)) {
16519
- dataBuffer = await decompressAsync(dataBuffer);
16520
- }
16521
- newImg.hdr = await readHeaderAsync(dataBuffer);
16522
- if (newImg.hdr !== null) {
16523
- if (newImg.hdr.cal_min === 0 && newImg.hdr.cal_max === 255) {
16524
- newImg.hdr.cal_max = 0;
16525
- }
16526
- if (isCompressed(dataBuffer)) {
16527
- imgRaw = readImage(newImg.hdr, await decompressAsync(dataBuffer));
16528
- } else {
16529
- imgRaw = readImage(newImg.hdr, dataBuffer);
16530
- }
17419
+ imgRaw = await nii_exports.readNifti(newImg, dataBuffer);
17420
+ if (imgRaw === null) {
17421
+ throw new Error(`Failed to parse NIfTI file ${name}.`);
16531
17422
  }
16532
17423
  break;
16533
17424
  default:
@@ -16561,8 +17452,8 @@ var NVImage = class _NVImage {
16561
17452
  // detect difference between voxel grid and world space
16562
17453
  // https://github.com/afni/afni/blob/25e77d564f2c67ff480fa99a7b8e48ec2d9a89fc/src/thd_coords.c#L717
16563
17454
  computeObliqueAngle(mtx44) {
16564
- const mtx = mat44.clone(mtx44);
16565
- mat44.transpose(mtx, mtx44);
17455
+ const mtx = mat46.clone(mtx44);
17456
+ mat46.transpose(mtx, mtx44);
16566
17457
  const dxtmp = Math.sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1] + mtx[2] * mtx[2]);
16567
17458
  const xmax = Math.max(Math.max(Math.abs(mtx[0]), Math.abs(mtx[1])), Math.abs(mtx[2])) / dxtmp;
16568
17459
  const dytmp = Math.sqrt(mtx[4] * mtx[4] + mtx[5] * mtx[5] + mtx[6] * mtx[6]);
@@ -16664,10 +17555,10 @@ var NVImage = class _NVImage {
16664
17555
  const X1mm = this.vox2mm([1 / this.pixDimsRAS[1], 0, 0], this.matRAS);
16665
17556
  const Y1mm = this.vox2mm([0, 1 / this.pixDimsRAS[2], 0], this.matRAS);
16666
17557
  const Z1mm = this.vox2mm([0, 0, 1 / this.pixDimsRAS[3]], this.matRAS);
16667
- vec36.subtract(X1mm, X1mm, LPI);
16668
- vec36.subtract(Y1mm, Y1mm, LPI);
16669
- vec36.subtract(Z1mm, Z1mm, LPI);
16670
- const oblique = mat44.fromValues(
17558
+ vec38.subtract(X1mm, X1mm, LPI);
17559
+ vec38.subtract(Y1mm, Y1mm, LPI);
17560
+ vec38.subtract(Z1mm, Z1mm, LPI);
17561
+ const oblique = mat46.fromValues(
16671
17562
  X1mm[0],
16672
17563
  X1mm[1],
16673
17564
  X1mm[2],
@@ -16685,19 +17576,19 @@ var NVImage = class _NVImage {
16685
17576
  0,
16686
17577
  1
16687
17578
  );
16688
- this.obliqueRAS = mat44.clone(oblique);
16689
- const XY = Math.abs(90 - vec36.angle(X1mm, Y1mm) * (180 / Math.PI));
16690
- const XZ = Math.abs(90 - vec36.angle(X1mm, Z1mm) * (180 / Math.PI));
16691
- const YZ = Math.abs(90 - vec36.angle(Y1mm, Z1mm) * (180 / Math.PI));
17579
+ this.obliqueRAS = mat46.clone(oblique);
17580
+ const XY = Math.abs(90 - vec38.angle(X1mm, Y1mm) * (180 / Math.PI));
17581
+ const XZ = Math.abs(90 - vec38.angle(X1mm, Z1mm) * (180 / Math.PI));
17582
+ const YZ = Math.abs(90 - vec38.angle(Y1mm, Z1mm) * (180 / Math.PI));
16692
17583
  this.maxShearDeg = Math.max(Math.max(XY, XZ), YZ);
16693
17584
  if (this.maxShearDeg > 0.1) {
16694
17585
  log.warn("Warning: voxels are rhomboidal, maximum shear is %f degrees.", this.maxShearDeg);
16695
17586
  }
16696
- const dim = vec44.fromValues(this.dimsRAS[1], this.dimsRAS[2], this.dimsRAS[3], 1);
16697
- const sform = mat44.clone(this.matRAS);
16698
- mat44.transpose(sform, sform);
16699
- const shim = vec44.fromValues(-0.5, -0.5, -0.5, 0);
16700
- mat44.translate(sform, sform, vec36.fromValues(shim[0], shim[1], shim[2]));
17587
+ const dim = vec45.fromValues(this.dimsRAS[1], this.dimsRAS[2], this.dimsRAS[3], 1);
17588
+ const sform = mat46.clone(this.matRAS);
17589
+ mat46.transpose(sform, sform);
17590
+ const shim = vec45.fromValues(-0.5, -0.5, -0.5, 0);
17591
+ mat46.translate(sform, sform, vec38.fromValues(shim[0], shim[1], shim[2]));
16701
17592
  sform[0] *= dim[0];
16702
17593
  sform[1] *= dim[0];
16703
17594
  sform[2] *= dim[0];
@@ -16707,11 +17598,11 @@ var NVImage = class _NVImage {
16707
17598
  sform[8] *= dim[2];
16708
17599
  sform[9] *= dim[2];
16709
17600
  sform[10] *= dim[2];
16710
- this.frac2mm = mat44.clone(sform);
17601
+ this.frac2mm = mat46.clone(sform);
16711
17602
  const pixdimX = this.pixDimsRAS[1];
16712
17603
  const pixdimY = this.pixDimsRAS[2];
16713
17604
  const pixdimZ = this.pixDimsRAS[3];
16714
- const oform = mat44.clone(sform);
17605
+ const oform = mat46.clone(sform);
16715
17606
  oform[0] = pixdimX * dim[0];
16716
17607
  oform[1] = 0;
16717
17608
  oform[2] = 0;
@@ -16725,11 +17616,11 @@ var NVImage = class _NVImage {
16725
17616
  oform[12] = (-originVoxel[0] - 0.5) * pixdimX;
16726
17617
  oform[13] = (-originVoxel[1] - 0.5) * pixdimY;
16727
17618
  oform[14] = (-originVoxel[2] - 0.5) * pixdimZ;
16728
- this.frac2mmOrtho = mat44.clone(oform);
17619
+ this.frac2mmOrtho = mat46.clone(oform);
16729
17620
  this.extentsMinOrtho = [oform[12], oform[13], oform[14]];
16730
17621
  this.extentsMaxOrtho = [oform[0] + oform[12], oform[5] + oform[13], oform[10] + oform[14]];
16731
- this.mm2ortho = mat44.create();
16732
- mat44.invert(this.mm2ortho, oblique);
17622
+ this.mm2ortho = mat46.create();
17623
+ mat46.invert(this.mm2ortho, oblique);
16733
17624
  }
16734
17625
  // not included in public docs
16735
17626
  // convert AFNI head/brik space to NIfTI format
@@ -16788,7 +17679,7 @@ var NVImage = class _NVImage {
16788
17679
  throw new Error("hdr not defined");
16789
17680
  }
16790
17681
  const m = this.hdr.affine;
16791
- const mat = mat44.fromValues(
17682
+ const mat = mat46.fromValues(
16792
17683
  m[0][0],
16793
17684
  m[0][1],
16794
17685
  m[0][2],
@@ -16808,14 +17699,14 @@ var NVImage = class _NVImage {
16808
17699
  );
16809
17700
  const mm000 = this.vox2mm([0, 0, 0], mat);
16810
17701
  const mm100 = this.vox2mm([1, 0, 0], mat);
16811
- vec36.subtract(mm100, mm100, mm000);
17702
+ vec38.subtract(mm100, mm100, mm000);
16812
17703
  const mm010 = this.vox2mm([0, 1, 0], mat);
16813
- vec36.subtract(mm010, mm010, mm000);
17704
+ vec38.subtract(mm010, mm010, mm000);
16814
17705
  const mm001 = this.vox2mm([0, 0, 1], mat);
16815
- vec36.subtract(mm001, mm001, mm000);
16816
- this.hdr.pixDims[1] = vec36.length(mm100);
16817
- this.hdr.pixDims[2] = vec36.length(mm010);
16818
- this.hdr.pixDims[3] = vec36.length(mm001);
17706
+ vec38.subtract(mm001, mm001, mm000);
17707
+ this.hdr.pixDims[1] = vec38.length(mm100);
17708
+ this.hdr.pixDims[2] = vec38.length(mm010);
17709
+ this.hdr.pixDims[3] = vec38.length(mm001);
16819
17710
  }
16820
17711
  // not included in public docs
16821
17712
  // read DICOM format image and treat it like a NIfTI
@@ -16950,7 +17841,7 @@ var NVImage = class _NVImage {
16950
17841
  // read ECAT7 format image
16951
17842
  // https://github.com/openneuropet/PET2BIDS/tree/28aae3fab22309047d36d867c624cd629c921ca6/ecat_validation/ecat_info
16952
17843
  readECAT(buffer) {
16953
- this.hdr = new NIFTI1();
17844
+ this.hdr = new NIFTI14();
16954
17845
  const hdr = this.hdr;
16955
17846
  hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
16956
17847
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
@@ -17046,11 +17937,11 @@ var NVImage = class _NVImage {
17046
17937
  ];
17047
17938
  hdr.numBitsPerVoxel = 32;
17048
17939
  hdr.datatypeCode = 16 /* DT_FLOAT32 */;
17049
- return rawImg;
17940
+ return rawImg.buffer;
17050
17941
  }
17051
17942
  // readECAT()
17052
17943
  readV16(buffer) {
17053
- this.hdr = new NIFTI1();
17944
+ this.hdr = new NIFTI14();
17054
17945
  const hdr = this.hdr;
17055
17946
  hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
17056
17947
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
@@ -17075,6 +17966,101 @@ var NVImage = class _NVImage {
17075
17966
  return buffer.slice(6);
17076
17967
  }
17077
17968
  // readV16()
17969
+ async readNPY(buffer) {
17970
+ function getTypeSize(dtype2) {
17971
+ const typeMap = {
17972
+ "|b1": 1,
17973
+ // Boolean
17974
+ "<i1": 1,
17975
+ // Int8
17976
+ "<u1": 1,
17977
+ // UInt8
17978
+ "<i2": 2,
17979
+ // Int16
17980
+ "<u2": 2,
17981
+ // UInt16
17982
+ "<i4": 4,
17983
+ // Int32
17984
+ "<u4": 4,
17985
+ // UInt32
17986
+ "<f4": 4,
17987
+ // Float32
17988
+ "<f8": 8
17989
+ // Float64
17990
+ };
17991
+ return typeMap[dtype2] ?? 1;
17992
+ }
17993
+ function getDataTypeCode(dtype2) {
17994
+ const typeMap = {
17995
+ "|b1": 2,
17996
+ // DT_BINARY
17997
+ "<i1": 256,
17998
+ // DT_INT8
17999
+ "<u1": 2,
18000
+ // DT_UINT8
18001
+ "<i2": 4,
18002
+ // DT_INT16
18003
+ "<u2": 512,
18004
+ // DT_UINT16
18005
+ "<i4": 8,
18006
+ // DT_INT32
18007
+ "<u4": 768,
18008
+ // DT_UINT32
18009
+ "<f4": 16,
18010
+ // DT_FLOAT32
18011
+ "<f8": 64
18012
+ // DT_FLOAT64
18013
+ };
18014
+ return typeMap[dtype2] ?? 16;
18015
+ }
18016
+ const dv = new DataView(buffer);
18017
+ const magicBytes = [dv.getUint8(0), dv.getUint8(1), dv.getUint8(2), dv.getUint8(3), dv.getUint8(4), dv.getUint8(5)];
18018
+ const expectedMagic = [147, 78, 85, 77, 80, 89];
18019
+ if (!magicBytes.every((byte, i) => byte === expectedMagic[i])) {
18020
+ throw new Error("Not a valid NPY file: Magic number mismatch");
18021
+ }
18022
+ const headerLen = dv.getUint16(8, true);
18023
+ const headerText = new TextDecoder("utf-8").decode(buffer.slice(10, 10 + headerLen));
18024
+ const shapeMatch = headerText.match(/'shape': \((.*?)\)/);
18025
+ if (!shapeMatch) {
18026
+ throw new Error("Invalid NPY header: Shape not found");
18027
+ }
18028
+ const shape = shapeMatch[1].split(",").map((s) => s.trim()).filter((s) => s !== "").map(Number);
18029
+ const dtypeMatch = headerText.match(/'descr': '([^']+)'/);
18030
+ if (!dtypeMatch) {
18031
+ throw new Error("Invalid NPY header: Data type not found");
18032
+ }
18033
+ const dtype = dtypeMatch[1];
18034
+ const numElements = shape.reduce((a, b) => a * b, 1);
18035
+ const dataStart = 10 + headerLen;
18036
+ const dataBuffer = buffer.slice(dataStart, dataStart + numElements * getTypeSize(dtype));
18037
+ const width = shape.length > 0 ? shape[shape.length - 1] : 1;
18038
+ const height = shape.length > 1 ? shape[shape.length - 2] : 1;
18039
+ const slices = shape.length > 2 ? shape[shape.length - 3] : 1;
18040
+ this.hdr = new NIFTI14();
18041
+ const hdr = this.hdr;
18042
+ hdr.dims = [3, width, height, slices, 0, 0, 0, 0];
18043
+ hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
18044
+ hdr.affine = [
18045
+ [hdr.pixDims[1], 0, 0, -(hdr.dims[1] - 2) * 0.5 * hdr.pixDims[1]],
18046
+ [0, -hdr.pixDims[2], 0, (hdr.dims[2] - 2) * 0.5 * hdr.pixDims[2]],
18047
+ [0, 0, -hdr.pixDims[3], (hdr.dims[3] - 2) * 0.5 * hdr.pixDims[3]],
18048
+ [0, 0, 0, 1]
18049
+ ];
18050
+ hdr.numBitsPerVoxel = getTypeSize(dtype) * 8;
18051
+ hdr.datatypeCode = getDataTypeCode(dtype);
18052
+ return dataBuffer;
18053
+ }
18054
+ async readNPZ(buffer) {
18055
+ const zip = new Zip(buffer);
18056
+ for (let i = 0; i < zip.entries.length; i++) {
18057
+ const entry = zip.entries[i];
18058
+ if (entry.fileName.toLowerCase().endsWith(".npy")) {
18059
+ const data = await entry.extract();
18060
+ return await this.readNPY(data.buffer);
18061
+ }
18062
+ }
18063
+ }
17078
18064
  async imageDataFromArrayBuffer(buffer) {
17079
18065
  return new Promise((resolve, reject) => {
17080
18066
  const blob = new Blob([buffer]);
@@ -17104,7 +18090,7 @@ var NVImage = class _NVImage {
17104
18090
  async readBMP(buffer) {
17105
18091
  const imageData = await this.imageDataFromArrayBuffer(buffer);
17106
18092
  const { width, height, data } = imageData;
17107
- this.hdr = new NIFTI1();
18093
+ this.hdr = new NIFTI14();
17108
18094
  const hdr = this.hdr;
17109
18095
  hdr.dims = [3, width, height, 1, 0, 0, 0, 0];
17110
18096
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
@@ -17116,13 +18102,28 @@ var NVImage = class _NVImage {
17116
18102
  ];
17117
18103
  hdr.numBitsPerVoxel = 8;
17118
18104
  hdr.datatypeCode = 2304 /* DT_RGBA32 */;
18105
+ let isGrayscale = true;
18106
+ for (let i = 0; i < data.length; i += 4) {
18107
+ if (data[i] !== data[i + 1] || data[i] !== data[i + 2]) {
18108
+ isGrayscale = false;
18109
+ break;
18110
+ }
18111
+ }
18112
+ if (isGrayscale) {
18113
+ hdr.datatypeCode = 2 /* DT_UINT8 */;
18114
+ const grayscaleData = new Uint8Array(width * height);
18115
+ for (let i = 0, j = 0; i < data.length; i += 4, j++) {
18116
+ grayscaleData[j] = data[i];
18117
+ }
18118
+ return grayscaleData.buffer;
18119
+ }
17119
18120
  return data.buffer;
17120
18121
  }
17121
18122
  // not included in public docs
17122
18123
  // read brainvoyager format VMR image
17123
18124
  // https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/343-developer-guide-2-6-the-format-of-vmr-files
17124
18125
  readVMR(buffer) {
17125
- this.hdr = new NIFTI1();
18126
+ this.hdr = new NIFTI14();
17126
18127
  const hdr = this.hdr;
17127
18128
  hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
17128
18129
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
@@ -17176,119 +18177,20 @@ var NVImage = class _NVImage {
17176
18177
  }
17177
18178
  // readVMR()
17178
18179
  // not included in public docs
17179
- // read FreeSurfer MGH format image
17180
- async readMGH(buffer) {
17181
- this.hdr = new NIFTI1();
17182
- const hdr = this.hdr;
17183
- hdr.littleEndian = false;
17184
- hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
17185
- hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
17186
- let raw = buffer;
17187
- let reader = new DataView(raw);
17188
- if (reader.getUint8(0) === 31 && reader.getUint8(1) === 139) {
17189
- raw = await NVUtilities.decompressToBuffer(new Uint8Array(buffer));
17190
- reader = new DataView(raw);
17191
- }
17192
- const version2 = reader.getInt32(0, false);
17193
- const width = reader.getInt32(4, false);
17194
- const height = reader.getInt32(8, false);
17195
- const depth = reader.getInt32(12, false);
17196
- const nframes = reader.getInt32(16, false);
17197
- const mtype = reader.getInt32(20, false);
17198
- const spacingX = reader.getFloat32(30, false);
17199
- const spacingY = reader.getFloat32(34, false);
17200
- const spacingZ = reader.getFloat32(38, false);
17201
- const xr = reader.getFloat32(42, false);
17202
- const xa = reader.getFloat32(46, false);
17203
- const xs = reader.getFloat32(50, false);
17204
- const yr = reader.getFloat32(54, false);
17205
- const ya = reader.getFloat32(58, false);
17206
- const ys = reader.getFloat32(62, false);
17207
- const zr = reader.getFloat32(66, false);
17208
- const za = reader.getFloat32(70, false);
17209
- const zs = reader.getFloat32(74, false);
17210
- const cr = reader.getFloat32(78, false);
17211
- const ca = reader.getFloat32(82, false);
17212
- const cs = reader.getFloat32(86, false);
17213
- if (version2 !== 1 || mtype < 0 || mtype > 4) {
17214
- log.warn("Not a valid MGH file");
17215
- }
17216
- if (mtype === 0) {
17217
- hdr.numBitsPerVoxel = 8;
17218
- hdr.datatypeCode = 2 /* DT_UINT8 */;
17219
- } else if (mtype === 4) {
17220
- hdr.numBitsPerVoxel = 16;
17221
- hdr.datatypeCode = 4 /* DT_INT16 */;
17222
- } else if (mtype === 1) {
17223
- hdr.numBitsPerVoxel = 32;
17224
- hdr.datatypeCode = 8 /* DT_INT32 */;
17225
- } else if (mtype === 3) {
17226
- hdr.numBitsPerVoxel = 32;
17227
- hdr.datatypeCode = 16 /* DT_FLOAT32 */;
17228
- }
17229
- hdr.dims[1] = width;
17230
- hdr.dims[2] = height;
17231
- hdr.dims[3] = depth;
17232
- hdr.dims[4] = nframes;
17233
- if (nframes > 1) {
17234
- hdr.dims[0] = 4;
17235
- }
17236
- hdr.pixDims[1] = spacingX;
17237
- hdr.pixDims[2] = spacingY;
17238
- hdr.pixDims[3] = spacingZ;
17239
- hdr.vox_offset = 284;
17240
- hdr.sform_code = 1;
17241
- const rot44 = mat44.fromValues(
17242
- xr * hdr.pixDims[1],
17243
- yr * hdr.pixDims[2],
17244
- zr * hdr.pixDims[3],
17245
- 0,
17246
- xa * hdr.pixDims[1],
17247
- ya * hdr.pixDims[2],
17248
- za * hdr.pixDims[3],
17249
- 0,
17250
- xs * hdr.pixDims[1],
17251
- ys * hdr.pixDims[2],
17252
- zs * hdr.pixDims[3],
17253
- 0,
17254
- 0,
17255
- 0,
17256
- 0,
17257
- 1
17258
- );
17259
- const Pcrs = [hdr.dims[1] / 2, hdr.dims[2] / 2, hdr.dims[3] / 2, 1];
17260
- const PxyzOffset = [0, 0, 0, 0];
17261
- for (let i = 0; i < 3; i++) {
17262
- PxyzOffset[i] = 0;
17263
- for (let j = 0; j < 3; j++) {
17264
- PxyzOffset[i] = PxyzOffset[i] + rot44[j + i * 4] * Pcrs[j];
17265
- }
17266
- }
17267
- hdr.affine = [
17268
- [rot44[0], rot44[1], rot44[2], cr - PxyzOffset[0]],
17269
- [rot44[4], rot44[5], rot44[6], ca - PxyzOffset[1]],
17270
- [rot44[8], rot44[9], rot44[10], cs - PxyzOffset[2]],
17271
- [0, 0, 0, 1]
17272
- ];
17273
- const nBytes = hdr.dims[1] * hdr.dims[2] * hdr.dims[3] * hdr.dims[4] * (hdr.numBitsPerVoxel / 8);
17274
- return raw.slice(hdr.vox_offset, hdr.vox_offset + nBytes);
17275
- }
17276
- // readMGH()
17277
- // not included in public docs
17278
18180
  // read DSI-Studio FIB format image
17279
18181
  // https://dsi-studio.labsolver.org/doc/cli_data.html
17280
18182
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17281
18183
  async readFIB(buffer) {
17282
- this.hdr = new NIFTI1();
18184
+ this.hdr = new NIFTI14();
17283
18185
  const hdr = this.hdr;
17284
18186
  hdr.littleEndian = false;
17285
18187
  hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
17286
18188
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
17287
- const mat = await NVUtilities.readMatV4(buffer);
18189
+ const mat = await NVUtilities.readMatV4(buffer, true);
17288
18190
  if (!("dimension" in mat) || !("dti_fa" in mat)) {
17289
18191
  throw new Error("Not a valid DSIstudio FIB file");
17290
18192
  }
17291
- const hasV1 = "index0" in mat && "index1" in mat && "index2" in mat;
18193
+ const hasV1 = "index0" in mat && "index1" in mat && "index2" in mat && "odf_vertices" in mat;
17292
18194
  hdr.numBitsPerVoxel = 32;
17293
18195
  hdr.datatypeCode = 16 /* DT_FLOAT32 */;
17294
18196
  hdr.dims[1] = mat.dimension[0];
@@ -17330,13 +18232,35 @@ var NVImage = class _NVImage {
17330
18232
  buff8v1.set(new Uint8Array(dir1.buffer, dir1.byteOffset, dir1.byteLength), 1 * nBytes3D);
17331
18233
  buff8v1.set(new Uint8Array(dir2.buffer, dir2.byteOffset, dir2.byteLength), 2 * nBytes3D);
17332
18234
  }
18235
+ if ("report" in mat) {
18236
+ hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)));
18237
+ }
17333
18238
  const buff8 = new Uint8Array(new ArrayBuffer(nBytes));
17334
18239
  const arrFA = Float32Array.from(mat.dti_fa);
18240
+ if ("mask" in mat) {
18241
+ console.log(mat);
18242
+ let slope = 1;
18243
+ if ("dti_fa_slope" in mat) {
18244
+ slope = mat.dti_fa_slope[0];
18245
+ }
18246
+ let inter = 1;
18247
+ if ("dti_fa_inter" in mat) {
18248
+ inter = mat.dti_fa_inter[0];
18249
+ }
18250
+ const nvox = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
18251
+ const mask = mat.mask;
18252
+ const f32 = new Float32Array(nvox);
18253
+ let j = 0;
18254
+ for (let i = 0; i < nvox; i++) {
18255
+ if (mask[i] !== 0) {
18256
+ f32[i] = arrFA[j] * slope + inter;
18257
+ j++;
18258
+ }
18259
+ }
18260
+ return [f32.buffer, new Float32Array(buff8v1.buffer)];
18261
+ }
17335
18262
  const imgFA = new Uint8Array(arrFA.buffer, arrFA.byteOffset, arrFA.byteLength);
17336
18263
  buff8.set(imgFA, 0);
17337
- if ("report" in mat) {
17338
- hdr.description = new TextDecoder().decode(mat.report.subarray(0, Math.min(79, mat.report.byteLength)));
17339
- }
17340
18264
  return [buff8.buffer, new Float32Array(buff8v1.buffer)];
17341
18265
  }
17342
18266
  // readFIB()
@@ -17345,7 +18269,7 @@ var NVImage = class _NVImage {
17345
18269
  // https://dsi-studio.labsolver.org/doc/cli_data.html
17346
18270
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17347
18271
  async readSRC(buffer) {
17348
- this.hdr = new NIFTI1();
18272
+ this.hdr = new NIFTI14();
17349
18273
  const hdr = this.hdr;
17350
18274
  hdr.littleEndian = false;
17351
18275
  hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
@@ -17415,7 +18339,7 @@ var NVImage = class _NVImage {
17415
18339
  // not included in public docs
17416
18340
  // read AFNI head/brik format image
17417
18341
  async readHEAD(dataBuffer, pairedImgData) {
17418
- this.hdr = new NIFTI1();
18342
+ this.hdr = new NIFTI14();
17419
18343
  const hdr = this.hdr;
17420
18344
  hdr.dims[0] = 3;
17421
18345
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
@@ -17427,9 +18351,8 @@ var NVImage = class _NVImage {
17427
18351
  const mod = (dataBuffer.byteLength + 8) % 16;
17428
18352
  const len = dataBuffer.byteLength + (16 - mod);
17429
18353
  log.debug(dataBuffer.byteLength, "len", len);
17430
- const extBuffer = new Uint8Array(len);
17431
- extBuffer.fill(0);
17432
- extBuffer.set(new Uint8Array(dataBuffer));
18354
+ const extBuffer = new ArrayBuffer(len);
18355
+ new Uint8Array(extBuffer).set(new Uint8Array(dataBuffer));
17433
18356
  const newExtension = new NIFTIEXTENSION(len + 8, 42, extBuffer, true);
17434
18357
  hdr.addExtension(newExtension);
17435
18358
  hdr.extensionCode = 42;
@@ -17509,7 +18432,7 @@ var NVImage = class _NVImage {
17509
18432
  hdr.affine = [
17510
18433
  [-items[0], -items[1], -items[2], -items[3]],
17511
18434
  [-items[4], -items[5], -items[6], -items[7]],
17512
- // TODO don't re-use items for numeric values
18435
+ // TODO don't reuse items for numeric values
17513
18436
  [items[8], items[9], items[10], items[11]],
17514
18437
  [0, 0, 0, 1]
17515
18438
  ];
@@ -17577,15 +18500,15 @@ var NVImage = class _NVImage {
17577
18500
  return new TextDecoder().decode(buffer.slice(startPos, pos));
17578
18501
  }
17579
18502
  let line = readStr();
17580
- this.hdr = new NIFTI1();
18503
+ this.hdr = new NIFTI14();
17581
18504
  const hdr = this.hdr;
17582
18505
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
17583
18506
  hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1];
17584
18507
  hdr.littleEndian = true;
17585
18508
  let isGz = false;
17586
18509
  let isDetached = false;
17587
- const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
17588
- const offset = vec36.fromValues(0, 0, 0);
18510
+ const mat33 = mat32.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
18511
+ const offset = vec38.fromValues(0, 0, 0);
17589
18512
  while (line !== "") {
17590
18513
  let items = line.split(" ");
17591
18514
  if (items.length > 2) {
@@ -17670,8 +18593,8 @@ var NVImage = class _NVImage {
17670
18593
  }
17671
18594
  line = readStr();
17672
18595
  }
17673
- const mmMat = mat3.fromValues(hdr.pixDims[1], 0, 0, 0, hdr.pixDims[2], 0, 0, 0, hdr.pixDims[3]);
17674
- mat3.multiply(mat33, mat33, mmMat);
18596
+ const mmMat = mat32.fromValues(hdr.pixDims[1], 0, 0, 0, hdr.pixDims[2], 0, 0, 0, hdr.pixDims[3]);
18597
+ mat32.multiply(mat33, mat33, mmMat);
17675
18598
  hdr.affine = [
17676
18599
  [-mat33[0], -mat33[3], -mat33[6], -offset[0]],
17677
18600
  [-mat33[1], -mat33[4], -mat33[7], -offset[1]],
@@ -17698,7 +18621,7 @@ var NVImage = class _NVImage {
17698
18621
  // read mrtrix MIF format image
17699
18622
  // https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#mrtrix-image-formats
17700
18623
  async readMIF(buffer, pairedImgData) {
17701
- this.hdr = new NIFTI1();
18624
+ this.hdr = new NIFTI14();
17702
18625
  const hdr = this.hdr;
17703
18626
  hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
17704
18627
  hdr.dims = [1, 1, 1, 1, 1, 1, 1, 1];
@@ -17986,290 +18909,10 @@ var NVImage = class _NVImage {
17986
18909
  }
17987
18910
  }
17988
18911
  }
17989
- return outVs;
18912
+ return outVs.buffer;
17990
18913
  }
17991
18914
  // readMIF()
17992
18915
  // not included in public docs
17993
- // read NRRD format image
17994
- // http://teem.sourceforge.net/nrrd/format.html
17995
- async readNRRD(dataBuffer, pairedImgData) {
17996
- this.hdr = new NIFTI1();
17997
- const hdr = this.hdr;
17998
- hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
17999
- const len = dataBuffer.byteLength;
18000
- let txt = null;
18001
- const bytes = new Uint8Array(dataBuffer);
18002
- for (let i = 1; i < len; i++) {
18003
- if (bytes[i - 1] === 10 && bytes[i] === 10) {
18004
- const v = dataBuffer.slice(0, i - 1);
18005
- txt = new TextDecoder().decode(v);
18006
- hdr.vox_offset = i + 1;
18007
- break;
18008
- }
18009
- }
18010
- if (txt === null) {
18011
- throw new Error("could not extract txt");
18012
- }
18013
- const lines = txt.split("\n");
18014
- if (!lines[0].startsWith("NRRD")) {
18015
- alert("Invalid NRRD image");
18016
- }
18017
- const n = lines.length;
18018
- let isGz = false;
18019
- let isMicron = false;
18020
- let isDetached = false;
18021
- const mat33 = mat3.fromValues(NaN, 0, 0, 0, 1, 0, 0, 0, 1);
18022
- const offset = vec36.fromValues(0, 0, 0);
18023
- let rot33 = mat3.create();
18024
- for (let i = 1; i < n; i++) {
18025
- let str = lines[i];
18026
- if (str[0] === "#") {
18027
- continue;
18028
- }
18029
- str = str.toLowerCase();
18030
- const items = str.split(":");
18031
- if (items.length < 2) {
18032
- continue;
18033
- }
18034
- const key = items[0].trim();
18035
- let value = items[1].trim();
18036
- value = value.replaceAll(")", " ");
18037
- value = value.replaceAll("(", " ");
18038
- value = value.trim();
18039
- switch (key) {
18040
- case "data file":
18041
- isDetached = true;
18042
- break;
18043
- case "encoding":
18044
- if (value.includes("raw")) {
18045
- isGz = false;
18046
- } else if (value.includes("gz")) {
18047
- isGz = true;
18048
- } else {
18049
- alert("Unsupported NRRD encoding");
18050
- }
18051
- break;
18052
- case "type":
18053
- switch (value) {
18054
- case "uchar":
18055
- case "unsigned char":
18056
- case "uint8":
18057
- case "uint8_t":
18058
- hdr.numBitsPerVoxel = 8;
18059
- hdr.datatypeCode = 2 /* DT_UINT8 */;
18060
- break;
18061
- case "signed char":
18062
- case "int8":
18063
- case "int8_t":
18064
- hdr.numBitsPerVoxel = 8;
18065
- hdr.datatypeCode = 256 /* DT_INT8 */;
18066
- break;
18067
- case "short":
18068
- case "short int":
18069
- case "signed short":
18070
- case "signed short int":
18071
- case "int16":
18072
- case "int16_t":
18073
- hdr.numBitsPerVoxel = 16;
18074
- hdr.datatypeCode = 4 /* DT_INT16 */;
18075
- break;
18076
- case "ushort":
18077
- case "unsigned short":
18078
- case "unsigned short int":
18079
- case "uint16":
18080
- case "uint16_t":
18081
- hdr.numBitsPerVoxel = 16;
18082
- hdr.datatypeCode = 512 /* DT_UINT16 */;
18083
- break;
18084
- case "int":
18085
- case "signed int":
18086
- case "int32":
18087
- case "int32_t":
18088
- hdr.numBitsPerVoxel = 32;
18089
- hdr.datatypeCode = 8 /* DT_INT32 */;
18090
- break;
18091
- case "uint":
18092
- case "unsigned int":
18093
- case "uint32":
18094
- case "uint32_t":
18095
- hdr.numBitsPerVoxel = 32;
18096
- hdr.datatypeCode = 768 /* DT_UINT32 */;
18097
- break;
18098
- case "float":
18099
- hdr.numBitsPerVoxel = 32;
18100
- hdr.datatypeCode = 16 /* DT_FLOAT32 */;
18101
- break;
18102
- case "double":
18103
- hdr.numBitsPerVoxel = 64;
18104
- hdr.datatypeCode = 64 /* DT_FLOAT64 */;
18105
- break;
18106
- default:
18107
- throw new Error("Unsupported NRRD data type: " + value);
18108
- }
18109
- break;
18110
- case "spacings":
18111
- {
18112
- const values = value.split(/[ ,]+/);
18113
- for (let d = 0; d < values.length; d++) {
18114
- hdr.pixDims[d + 1] = parseFloat(values[d]);
18115
- }
18116
- }
18117
- break;
18118
- case "sizes":
18119
- {
18120
- const dims = value.split(/[ ,]+/);
18121
- hdr.dims[0] = dims.length;
18122
- for (let d = 0; d < dims.length; d++) {
18123
- hdr.dims[d + 1] = parseInt(dims[d]);
18124
- }
18125
- }
18126
- break;
18127
- case "endian":
18128
- if (value.includes("little")) {
18129
- hdr.littleEndian = true;
18130
- } else if (value.includes("big")) {
18131
- hdr.littleEndian = false;
18132
- }
18133
- break;
18134
- case "space directions":
18135
- {
18136
- const vs = value.split(/[ ,]+/);
18137
- if (vs.length !== 9) {
18138
- break;
18139
- }
18140
- for (let d = 0; d < 9; d++) {
18141
- mat33[d] = parseFloat(vs[d]);
18142
- }
18143
- }
18144
- break;
18145
- case "space origin":
18146
- {
18147
- const ts = value.split(/[ ,]+/);
18148
- if (ts.length !== 3) {
18149
- break;
18150
- }
18151
- offset[0] = parseFloat(ts[0]);
18152
- offset[1] = parseFloat(ts[1]);
18153
- offset[2] = parseFloat(ts[2]);
18154
- }
18155
- break;
18156
- case "space units":
18157
- if (value.includes("microns")) {
18158
- isMicron = true;
18159
- }
18160
- break;
18161
- case "space":
18162
- if (value.includes("right-anterior-superior") || value.includes("ras")) {
18163
- rot33 = mat3.fromValues(
18164
- 1,
18165
- 0,
18166
- 0,
18167
- 0,
18168
- 1,
18169
- 0,
18170
- 0,
18171
- 0,
18172
- 1
18173
- );
18174
- } else if (value.includes("left-anterior-superior") || value.includes("las")) {
18175
- rot33 = mat3.fromValues(
18176
- -1,
18177
- 0,
18178
- 0,
18179
- 0,
18180
- 1,
18181
- 0,
18182
- 0,
18183
- 0,
18184
- 1
18185
- );
18186
- } else if (value.includes("left-posterior-superior") || value.includes("lps")) {
18187
- rot33 = mat3.fromValues(
18188
- -1,
18189
- 0,
18190
- 0,
18191
- 0,
18192
- -1,
18193
- 0,
18194
- 0,
18195
- 0,
18196
- 1
18197
- );
18198
- } else {
18199
- log.warn("Unsupported NRRD space value:", value);
18200
- }
18201
- break;
18202
- default:
18203
- log.warn("Unknown:", key);
18204
- }
18205
- }
18206
- if (!isNaN(mat33[0])) {
18207
- this.hdr.sform_code = 2;
18208
- if (isMicron) {
18209
- mat44.multiplyScalar(mat33, mat33, 1e-3);
18210
- offset[0] *= 1e-3;
18211
- offset[1] *= 1e-3;
18212
- offset[2] *= 1e-3;
18213
- }
18214
- if (rot33[0] < 0) {
18215
- offset[0] = -offset[0];
18216
- }
18217
- if (rot33[4] < 0) {
18218
- offset[1] = -offset[1];
18219
- }
18220
- if (rot33[8] < 0) {
18221
- offset[2] = -offset[2];
18222
- }
18223
- mat3.multiply(mat33, rot33, mat33);
18224
- const mat = mat44.fromValues(
18225
- mat33[0],
18226
- mat33[3],
18227
- mat33[6],
18228
- offset[0],
18229
- mat33[1],
18230
- mat33[4],
18231
- mat33[7],
18232
- offset[1],
18233
- mat33[2],
18234
- mat33[5],
18235
- mat33[8],
18236
- offset[2],
18237
- 0,
18238
- 0,
18239
- 0,
18240
- 1
18241
- );
18242
- const mm000 = this.vox2mm([0, 0, 0], mat);
18243
- const mm100 = this.vox2mm([1, 0, 0], mat);
18244
- vec36.subtract(mm100, mm100, mm000);
18245
- const mm010 = this.vox2mm([0, 1, 0], mat);
18246
- vec36.subtract(mm010, mm010, mm000);
18247
- const mm001 = this.vox2mm([0, 0, 1], mat);
18248
- vec36.subtract(mm001, mm001, mm000);
18249
- hdr.pixDims[1] = vec36.length(mm100);
18250
- hdr.pixDims[2] = vec36.length(mm010);
18251
- hdr.pixDims[3] = vec36.length(mm001);
18252
- hdr.affine = [
18253
- [mat[0], mat[1], mat[2], mat[3]],
18254
- [mat[4], mat[5], mat[6], mat[7]],
18255
- [mat[8], mat[9], mat[10], mat[11]],
18256
- [0, 0, 0, 1]
18257
- ];
18258
- }
18259
- if (isDetached && pairedImgData) {
18260
- return pairedImgData.slice(0);
18261
- }
18262
- if (isDetached) {
18263
- log.warn("Missing data: NRRD header describes detached data file but only one URL provided");
18264
- }
18265
- if (isGz) {
18266
- return await NVUtilities.decompressToBuffer(new Uint8Array(dataBuffer.slice(hdr.vox_offset)));
18267
- } else {
18268
- return dataBuffer.slice(hdr.vox_offset);
18269
- }
18270
- }
18271
- // readNRRD()
18272
- // not included in public docs
18273
18916
  // Transform to orient NIfTI image to Left->Right,Posterior->Anterior,Inferior->Superior (48 possible permutations)
18274
18917
  calculateRAS() {
18275
18918
  if (!this.hdr) {
@@ -18277,7 +18920,7 @@ var NVImage = class _NVImage {
18277
18920
  }
18278
18921
  const a = this.hdr.affine;
18279
18922
  const header = this.hdr;
18280
- const absR = mat3.fromValues(
18923
+ const absR = mat32.fromValues(
18281
18924
  Math.abs(a[0][0]),
18282
18925
  Math.abs(a[0][1]),
18283
18926
  Math.abs(a[0][2]),
@@ -18320,7 +18963,7 @@ var NVImage = class _NVImage {
18320
18963
  perm[ixyz[0] - 1] = 1;
18321
18964
  perm[ixyz[1] - 1] = 2;
18322
18965
  perm[ixyz[2] - 1] = 3;
18323
- let rotM = mat44.fromValues(
18966
+ let rotM = mat46.fromValues(
18324
18967
  a[0][0],
18325
18968
  a[0][1],
18326
18969
  a[0][2],
@@ -18342,8 +18985,8 @@ var NVImage = class _NVImage {
18342
18985
  this.mm100 = this.vox2mm([header.dims[1] - 0.5, -0.5, -0.5], rotM);
18343
18986
  this.mm010 = this.vox2mm([-0.5, header.dims[2] - 0.5, -0.5], rotM);
18344
18987
  this.mm001 = this.vox2mm([-0.5, -0.5, header.dims[3] - 0.5], rotM);
18345
- const R = mat44.create();
18346
- mat44.copy(R, rotM);
18988
+ const R = mat46.create();
18989
+ mat46.copy(R, rotM);
18347
18990
  for (let i = 0; i < 3; i++) {
18348
18991
  for (let j = 0; j < 3; j++) {
18349
18992
  R[i * 4 + j] = rotM[i * 4 + perm[j] - 1];
@@ -18368,32 +19011,32 @@ var NVImage = class _NVImage {
18368
19011
  }
18369
19012
  }
18370
19013
  if (this.arrayEquals(perm, [1, 2, 3]) && this.arrayEquals(flip, [0, 0, 0])) {
18371
- this.toRAS = mat44.create();
18372
- this.matRAS = mat44.clone(rotM);
19014
+ this.toRAS = mat46.create();
19015
+ this.matRAS = mat46.clone(rotM);
18373
19016
  this.calculateOblique();
18374
19017
  this.img2RASstep = [1, this.dimsRAS[1], this.dimsRAS[1] * this.dimsRAS[2]];
18375
19018
  this.img2RASstart = [0, 0, 0];
18376
19019
  return;
18377
19020
  }
18378
- mat44.identity(rotM);
19021
+ mat46.identity(rotM);
18379
19022
  rotM[0 + 0 * 4] = 1 - flip[0] * 2;
18380
19023
  rotM[1 + 1 * 4] = 1 - flip[1] * 2;
18381
19024
  rotM[2 + 2 * 4] = 1 - flip[2] * 2;
18382
19025
  rotM[3 + 0 * 4] = (header.dims[perm[0]] - 1) * flip[0];
18383
19026
  rotM[3 + 1 * 4] = (header.dims[perm[1]] - 1) * flip[1];
18384
19027
  rotM[3 + 2 * 4] = (header.dims[perm[2]] - 1) * flip[2];
18385
- const residualR = mat44.create();
18386
- mat44.invert(residualR, rotM);
18387
- mat44.multiply(residualR, residualR, R);
18388
- this.matRAS = mat44.clone(residualR);
18389
- rotM = mat44.fromValues(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
19028
+ const residualR = mat46.create();
19029
+ mat46.invert(residualR, rotM);
19030
+ mat46.multiply(residualR, residualR, R);
19031
+ this.matRAS = mat46.clone(residualR);
19032
+ rotM = mat46.fromValues(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
18390
19033
  rotM[perm[0] - 1 + 0 * 4] = -flip[0] * 2 + 1;
18391
19034
  rotM[perm[1] - 1 + 1 * 4] = -flip[1] * 2 + 1;
18392
19035
  rotM[perm[2] - 1 + 2 * 4] = -flip[2] * 2 + 1;
18393
19036
  rotM[3 + 0 * 4] = flip[0];
18394
19037
  rotM[3 + 1 * 4] = flip[1];
18395
19038
  rotM[3 + 2 * 4] = flip[2];
18396
- this.toRAS = mat44.clone(rotM);
19039
+ this.toRAS = mat46.clone(rotM);
18397
19040
  rotM[3] = 0;
18398
19041
  rotM[7] = 0;
18399
19042
  rotM[11] = 0;
@@ -18409,7 +19052,7 @@ var NVImage = class _NVImage {
18409
19052
  if (this.permRAS[0] === -3 || this.permRAS[1] === -3 || this.permRAS[2] === -3) {
18410
19053
  rotM[14] = header.dims[3] - 1;
18411
19054
  }
18412
- this.toRASvox = mat44.clone(rotM);
19055
+ this.toRASvox = mat46.clone(rotM);
18413
19056
  log.debug(this.hdr.dims);
18414
19057
  log.debug(this.dimsRAS);
18415
19058
  const hdr = this.hdr;
@@ -18439,7 +19082,7 @@ var NVImage = class _NVImage {
18439
19082
  throw new Error("hdr undefined");
18440
19083
  }
18441
19084
  const hdrBytes = hdrToArrayBuffer({ ...this.hdr, vox_offset: 352 }, false);
18442
- const hdr = await readHeaderAsync(hdrBytes.buffer, true);
19085
+ const hdr = await readHeaderAsync2(hdrBytes.buffer, true);
18443
19086
  if (nVolumes === 1) {
18444
19087
  hdr.dims[0] = 3;
18445
19088
  hdr.dims[4] = 1;
@@ -18521,11 +19164,11 @@ var NVImage = class _NVImage {
18521
19164
  // not included in public docs
18522
19165
  // convert voxel location (row, column slice, indexed from 0) to world space
18523
19166
  vox2mm(XYZ, mtx) {
18524
- const sform = mat44.clone(mtx);
18525
- mat44.transpose(sform, sform);
18526
- const pos = vec44.fromValues(XYZ[0], XYZ[1], XYZ[2], 1);
18527
- vec44.transformMat4(pos, pos, sform);
18528
- const pos3 = vec36.fromValues(pos[0], pos[1], pos[2]);
19167
+ const sform = mat46.clone(mtx);
19168
+ mat46.transpose(sform, sform);
19169
+ const pos = vec45.fromValues(XYZ[0], XYZ[1], XYZ[2], 1);
19170
+ vec45.transformMat4(pos, pos, sform);
19171
+ const pos3 = vec38.fromValues(pos[0], pos[1], pos[2]);
18529
19172
  return pos3;
18530
19173
  }
18531
19174
  // vox2mm()
@@ -18535,13 +19178,13 @@ var NVImage = class _NVImage {
18535
19178
  if (!this.matRAS) {
18536
19179
  throw new Error("matRAS undefined");
18537
19180
  }
18538
- const sform = mat44.clone(this.matRAS);
18539
- const out = mat44.clone(sform);
18540
- mat44.transpose(out, sform);
18541
- mat44.invert(out, out);
18542
- const pos = vec44.fromValues(mm[0], mm[1], mm[2], 1);
18543
- vec44.transformMat4(pos, pos, out);
18544
- const pos3 = vec36.fromValues(pos[0], pos[1], pos[2]);
19181
+ const sform = mat46.clone(this.matRAS);
19182
+ const out = mat46.clone(sform);
19183
+ mat46.transpose(out, sform);
19184
+ mat46.invert(out, out);
19185
+ const pos = vec45.fromValues(mm[0], mm[1], mm[2], 1);
19186
+ vec45.transformMat4(pos, pos, out);
19187
+ const pos3 = vec38.fromValues(pos[0], pos[1], pos[2]);
18545
19188
  if (frac) {
18546
19189
  return pos3;
18547
19190
  }
@@ -18836,58 +19479,26 @@ var NVImage = class _NVImage {
18836
19479
  }
18837
19480
  return (scaled - this.hdr.scl_inter) / this.hdr.scl_slope;
18838
19481
  }
18839
- // not included in public docs
18840
- // see niivue.saveImage() for wrapper of this function
19482
+ /**
19483
+ * Converts NVImage to NIfTI compliant byte array, potentially compressed.
19484
+ * Delegates to ImageWriter.saveToUint8Array.
19485
+ * @param fnm - Filename (determines if compression is needed via .gz suffix)
19486
+ * @param drawing8 - Optional Uint8Array drawing overlay
19487
+ * @returns Promise<Uint8Array>
19488
+ */
18841
19489
  async saveToUint8Array(fnm, drawing8 = null) {
18842
- if (!this.hdr) {
18843
- throw new Error("hdr undefined");
18844
- }
18845
- if (!this.img) {
18846
- throw new Error("img undefined");
18847
- }
18848
- const isDrawing8 = drawing8 !== null;
18849
- const hdrBytes = hdrToArrayBuffer(this.hdr, isDrawing8);
18850
- const opad = new Uint8Array(4);
18851
- let img8 = new Uint8Array(this.img.buffer);
18852
- if (isDrawing8) {
18853
- img8 = new Uint8Array(drawing8.buffer);
18854
- }
18855
- const odata = new Uint8Array(hdrBytes.length + opad.length + img8.length);
18856
- odata.set(hdrBytes);
18857
- odata.set(opad, hdrBytes.length);
18858
- odata.set(img8, hdrBytes.length + opad.length);
18859
- let saveData = null;
18860
- const compress = fnm.endsWith(".gz");
18861
- if (compress) {
18862
- saveData = new Uint8Array(await NVUtilities.compress(odata, "gzip"));
18863
- } else {
18864
- saveData = odata;
18865
- }
18866
- return saveData;
19490
+ return saveToUint8Array(this, fnm, drawing8);
18867
19491
  }
18868
- // not included in public docs
18869
- // save image as NIfTI volume
18870
- // if fnm is empty, data is returned
19492
+ /**
19493
+ * save image as NIfTI volume and trigger download.
19494
+ * Delegates to ImageWriter.saveToDisk.
19495
+ * @param fnm - Filename for download. If empty, returns data without download.
19496
+ * @param drawing8 - Optional Uint8Array drawing overlay
19497
+ * @returns Promise<Uint8Array>
19498
+ */
18871
19499
  async saveToDisk(fnm = "", drawing8 = null) {
18872
- const saveData = await this.saveToUint8Array(fnm, drawing8);
18873
- if (fnm === "") {
18874
- log.debug("saveToDisk: empty file name, returning data as Uint8Array rather than triggering download");
18875
- return saveData;
18876
- }
18877
- const blob = new Blob([saveData.buffer], {
18878
- type: "application/octet-stream"
18879
- });
18880
- const blobUrl = URL.createObjectURL(blob);
18881
- const link = document.createElement("a");
18882
- link.setAttribute("href", blobUrl);
18883
- link.setAttribute("download", fnm);
18884
- link.style.visibility = "hidden";
18885
- document.body.appendChild(link);
18886
- link.click();
18887
- document.body.removeChild(link);
18888
- return saveData;
19500
+ return saveToDisk(this, fnm, drawing8);
18889
19501
  }
18890
- // saveToDisk()
18891
19502
  static async fetchDicomData(url, headers = {}) {
18892
19503
  if (url === "") {
18893
19504
  throw Error("url must not be empty");
@@ -19007,7 +19618,7 @@ var NVImage = class _NVImage {
19007
19618
  if (!isNifti1) {
19008
19619
  return null;
19009
19620
  }
19010
- const hdr = await readHeaderAsync(hdrU8s.buffer);
19621
+ const hdr = await readHeaderAsync2(hdrU8s.buffer);
19011
19622
  if (!hdr) {
19012
19623
  throw new Error("Could not read NIfTI header");
19013
19624
  }
@@ -19064,7 +19675,7 @@ var NVImage = class _NVImage {
19064
19675
  }
19065
19676
  hdrU8s = concatU8s(hdrU8s, value2);
19066
19677
  }
19067
- const hdr = await readHeaderAsync(hdrU8s.buffer);
19678
+ const hdr = await readHeaderAsync2(hdrU8s.buffer);
19068
19679
  if (!hdr) {
19069
19680
  throw new Error("Could not read NIfTI header");
19070
19681
  }
@@ -19346,7 +19957,7 @@ var NVImage = class _NVImage {
19346
19957
  if (!isNifti1) {
19347
19958
  dataBuffer = await this.readFileAsync(file);
19348
19959
  } else {
19349
- const hdr = await readHeaderAsync(headerBuffer);
19960
+ const hdr = await readHeaderAsync2(headerBuffer);
19350
19961
  if (!hdr) {
19351
19962
  throw new Error("could not read nifti header");
19352
19963
  }
@@ -19393,70 +20004,19 @@ var NVImage = class _NVImage {
19393
20004
  return nvimage;
19394
20005
  }
19395
20006
  /**
19396
- * factory function to load and return a new NVImage instance from a base64 encoded string
19397
- *
19398
- * @returns NVImage instance
19399
- * @example
19400
- * myImage = NVImage.loadFromBase64('SomeBase64String')
20007
+ * Creates a Uint8Array representing a NIFTI file (header + optional image data).
20008
+ * Delegates to ImageWriter.createNiftiArray.
19401
20009
  */
19402
- static createNiftiArray(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2, img = new Uint8Array()) {
19403
- const hdr = this.createNiftiHeader(dims, pixDims, affine, datatypeCode);
19404
- const hdrBytes = hdrToArrayBuffer(hdr, false);
19405
- if (img.length < 1) {
19406
- return hdrBytes;
19407
- }
19408
- const opad = new Uint8Array(4);
19409
- const img8 = new Uint8Array(img.buffer);
19410
- const odata = new Uint8Array(hdrBytes.length + opad.length + img8.length);
19411
- odata.set(hdrBytes);
19412
- odata.set(opad, hdrBytes.length);
19413
- odata.set(img8, hdrBytes.length + opad.length);
19414
- return odata;
20010
+ static createNiftiArray(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */, img = new Uint8Array()) {
20011
+ return createNiftiArray(dims, pixDims, affine, datatypeCode, img);
19415
20012
  }
19416
- // createNiftiFile()
19417
- static createNiftiHeader(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2) {
19418
- const hdr = new NIFTI1();
19419
- hdr.littleEndian = true;
19420
- hdr.dims = [3, 1, 1, 1, 0, 0, 0, 0];
19421
- hdr.dims[0] = Math.max(3, dims.length);
19422
- for (let i = 0; i < dims.length; i++) {
19423
- hdr.dims[i + 1] = dims[i];
19424
- }
19425
- hdr.pixDims = [1, 1, 1, 1, 1, 0, 0, 0];
19426
- for (let i = 0; i < dims.length; i++) {
19427
- hdr.pixDims[i + 1] = pixDims[i];
19428
- }
19429
- if (affine.length === 16) {
19430
- let k = 0;
19431
- for (let i = 0; i < 4; i++) {
19432
- for (let j = 0; j < 4; j++) {
19433
- hdr.affine[i][j] = affine[k];
19434
- k++;
19435
- }
19436
- }
19437
- }
19438
- let bpv = 8;
19439
- if (datatypeCode === 256 || datatypeCode === 2) {
19440
- bpv = 8;
19441
- } else if (datatypeCode === 512 || datatypeCode === 4) {
19442
- bpv = 16;
19443
- } else if (datatypeCode === 16 || datatypeCode === 768 || datatypeCode === 8 || datatypeCode === 2304) {
19444
- bpv = 32;
19445
- } else if (datatypeCode === 64) {
19446
- bpv = 64;
19447
- } else {
19448
- log.warn("Unsupported NIfTI datatypeCode: " + datatypeCode);
19449
- }
19450
- hdr.datatypeCode = datatypeCode;
19451
- hdr.numBitsPerVoxel = bpv;
19452
- hdr.scl_inter = 0;
19453
- hdr.scl_slope = 0;
19454
- hdr.sform_code = 2;
19455
- hdr.magic = "n+1";
19456
- hdr.vox_offset = 352;
19457
- return hdr;
20013
+ /**
20014
+ * Creates a NIFTI1 header object with basic properties.
20015
+ * Delegates to ImageWriter.createNiftiHeader.
20016
+ */
20017
+ static createNiftiHeader(dims = [256, 256, 256], pixDims = [1, 1, 1], affine = [1, 0, 0, -128, 0, 1, 0, -128, 0, 0, 1, -128, 0, 0, 0, 1], datatypeCode = 2 /* DT_UINT8 */) {
20018
+ return createNiftiHeader(dims, pixDims, affine, datatypeCode);
19458
20019
  }
19459
- // loadFromHeader
19460
20020
  /**
19461
20021
  * read a 3D slab of voxels from a volume
19462
20022
  * @param voxStart - first row, column and slice (RAS order) for selection
@@ -19465,77 +20025,17 @@ var NVImage = class _NVImage {
19465
20025
  * @returns the an array where ret[0] is the voxel values and ret[1] is dimension of selection
19466
20026
  * @see {@link https://niivue.github.io/niivue/features/slab_selection.html | live demo usage}
19467
20027
  */
20028
+ /**
20029
+ * read a 3D slab of voxels from a volume, specified in RAS coordinates.
20030
+ * Delegates to VolumeUtils.getVolumeData.
20031
+ * @param voxStart - first row, column and slice (RAS order) for selection
20032
+ * @param voxEnd - final row, column and slice (RAS order) for selection
20033
+ * @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32', 'scaled', 'normalized', 'windowed'
20034
+ * @returns the an array where ret[0] is the voxel values and ret[1] is dimension of selection
20035
+ */
19468
20036
  getVolumeData(voxStart = [-1, 0, 0], voxEnd = [0, 0, 0], dataType = "same") {
19469
- let img = new Uint8Array();
19470
- if (Math.min(...voxStart) < 0 || Math.min(...voxEnd) < 0) {
19471
- return [img, [0, 0, 0]];
19472
- }
19473
- const dims = this.dimsRAS.slice(1, 4);
19474
- for (let i2 = 0; i2 < 3; i2++) {
19475
- voxStart[i2] = Math.min(voxStart[i2], dims[i2] - 1);
19476
- voxEnd[i2] = Math.min(voxEnd[i2], dims[i2] - 1);
19477
- if (voxEnd[i2] < voxStart[i2]) {
19478
- const tmp = voxEnd[i2];
19479
- voxEnd[i2] = voxStart[i2];
19480
- voxStart[i2] = tmp;
19481
- }
19482
- }
19483
- const slabDims = [voxEnd[0] - voxStart[0] + 1, voxEnd[1] - voxStart[1] + 1, voxEnd[2] - voxStart[2] + 1];
19484
- const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
19485
- let dt = this.hdr.datatypeCode;
19486
- if (dataType === "uint8") {
19487
- dt = 2 /* DT_UINT8 */;
19488
- } else if (dataType === "float32" || dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
19489
- dt = 16 /* DT_FLOAT32 */;
19490
- }
19491
- if (dt === 2 /* DT_UINT8 */) {
19492
- img = new Uint8Array(slabNVox);
19493
- } else if (dt === 4 /* DT_INT16 */) {
19494
- img = new Int16Array(slabNVox);
19495
- } else if (dt === 512 /* DT_UINT16 */) {
19496
- img = new Uint16Array(slabNVox);
19497
- } else if (dt === 16 /* DT_FLOAT32 */) {
19498
- img = new Float32Array(slabNVox);
19499
- } else if (dt === 64 /* DT_FLOAT64 */) {
19500
- img = new Float64Array(slabNVox);
19501
- } else {
19502
- log.error("getVolumeData unsupported datatype");
19503
- return [img, [0, 0, 0]];
19504
- }
19505
- const outStep = this.img2RASstep;
19506
- const outStart = this.img2RASstart;
19507
- let i = 0;
19508
- for (let z = voxStart[2]; z <= voxEnd[2]; z++) {
19509
- const zi = outStart[2] + z * outStep[2];
19510
- for (let y = voxStart[1]; y <= voxEnd[1]; y++) {
19511
- const yizi = zi + outStart[1] + y * outStep[1];
19512
- for (let x = voxStart[0]; x <= voxEnd[0]; x++) {
19513
- const xi = outStart[0] + x * outStep[0];
19514
- img[i++] = this.img[xi + yizi];
19515
- }
19516
- }
19517
- }
19518
- if (dataType === "scaled" || dataType === "normalized" || dataType === "windowed") {
19519
- for (let i2 = 0; i2 < img.length; i2++) {
19520
- img[i2] = img[i2] * this.hdr.scl_slope + this.hdr.scl_inter;
19521
- }
19522
- }
19523
- if (dataType === "normalized" || dataType === "windowed") {
19524
- let mn = this.cal_min;
19525
- let mx = this.cal_max;
19526
- if (dataType === "normalized") {
19527
- mn = this.global_min;
19528
- mx = this.global_max;
19529
- }
19530
- const scale = 1 / (mx - mn);
19531
- for (let i2 = 0; i2 < img.length; i2++) {
19532
- img[i2] = (img[i2] - mn) * scale;
19533
- img[i2] = Math.max(Math.min(img[i2], 1), 0);
19534
- }
19535
- }
19536
- return [img, slabDims];
20037
+ return getVolumeData(this, voxStart, voxEnd, dataType);
19537
20038
  }
19538
- // getVolumeData()
19539
20039
  /**
19540
20040
  * write a 3D slab of voxels from a volume
19541
20041
  * @param voxStart - first row, column and slice (RAS order) for selection
@@ -19543,41 +20043,17 @@ var NVImage = class _NVImage {
19543
20043
  * @param img - array of voxel values to insert (RAS order)
19544
20044
  * @see {@link https://niivue.github.io/niivue/features/slab_selection.html | live demo usage}
19545
20045
  */
20046
+ /**
20047
+ * write a 3D slab of voxels from a volume, specified in RAS coordinates.
20048
+ * Delegates to VolumeUtils.setVolumeData.
20049
+ * Input slabData is assumed to be in the correct raw data type for the target image.
20050
+ * @param voxStart - first row, column and slice (RAS order) for selection
20051
+ * @param voxEnd - final row, column and slice (RAS order) for selection
20052
+ * @param img - array of voxel values to insert (RAS order, raw data type)
20053
+ */
19546
20054
  setVolumeData(voxStart = [-1, 0, 0], voxEnd = [0, 0, 0], img = new Uint8Array()) {
19547
- if (img.length < 1 || Math.min(...voxStart) < 0 || Math.min(...voxEnd) < 0) {
19548
- return;
19549
- }
19550
- const dims = this.dimsRAS.slice(1, 4);
19551
- for (let i2 = 0; i2 < 3; i2++) {
19552
- voxStart[i2] = Math.min(voxStart[i2], dims[i2] - 1);
19553
- voxEnd[i2] = Math.min(voxEnd[i2], dims[i2] - 1);
19554
- if (voxEnd[i2] < voxStart[i2]) {
19555
- const tmp = voxEnd[i2];
19556
- voxEnd[i2] = voxStart[i2];
19557
- voxStart[i2] = tmp;
19558
- }
19559
- }
19560
- const slabDims = [voxEnd[0] - voxStart[0] + 1, voxEnd[1] - voxStart[1] + 1, voxEnd[2] - voxStart[2] + 1];
19561
- const slabNVox = slabDims[0] * slabDims[1] * slabDims[2];
19562
- if (img.length < slabNVox) {
19563
- log.error("setVolumeData image does not have enough voxels");
19564
- return;
19565
- }
19566
- const outStep = this.img2RASstep;
19567
- const outStart = this.img2RASstart;
19568
- let i = 0;
19569
- for (let z = voxStart[2]; z <= voxEnd[2]; z++) {
19570
- const zi = outStart[2] + z * outStep[2];
19571
- for (let y = voxStart[1]; y <= voxEnd[1]; y++) {
19572
- const yizi = zi + outStart[1] + y * outStep[1];
19573
- for (let x = voxStart[0]; x <= voxEnd[0]; x++) {
19574
- const xi = outStart[0] + x * outStep[0];
19575
- this.img[xi + yizi] = img[i++];
19576
- }
19577
- }
19578
- }
20055
+ setVolumeData(this, voxStart, voxEnd, img);
19579
20056
  }
19580
- // setVolumeData()
19581
20057
  /**
19582
20058
  * factory function to load and return a new NVImage instance from a base64 encoded string
19583
20059
  *
@@ -19728,155 +20204,18 @@ var NVImage = class _NVImage {
19728
20204
  }
19729
20205
  return zeroClone;
19730
20206
  }
19731
- // not included in public docs
19732
- // return voxel intensity at specific coordinates (xyz are zero indexed column row, slice)
19733
- getValue(x, y, z, frame4D = 0, isReadImaginary = false) {
19734
- if (!this.hdr) {
19735
- throw new Error("hdr undefined");
19736
- }
19737
- if (!this.img) {
19738
- throw new Error("img undefined");
19739
- }
19740
- const nx = this.hdr.dims[1];
19741
- const ny = this.hdr.dims[2];
19742
- const nz = this.hdr.dims[3];
19743
- const perm = this.permRAS.slice();
19744
- if (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3) {
19745
- const pos = vec44.fromValues(x, y, z, 1);
19746
- vec44.transformMat4(pos, pos, this.toRASvox);
19747
- x = pos[0];
19748
- y = pos[1];
19749
- z = pos[2];
19750
- }
19751
- let vx = x + y * nx + z * nx * ny;
19752
- if (this.hdr.datatypeCode === 2304 /* DT_RGBA32 */) {
19753
- vx *= 4;
19754
- return Math.round(this.img[vx] * 0.21 + this.img[vx + 1] * 0.72 + this.img[vx + 2] * 0.07);
19755
- }
19756
- if (this.hdr.datatypeCode === 128 /* DT_RGB24 */) {
19757
- vx *= 3;
19758
- return Math.round(this.img[vx] * 0.21 + this.img[vx + 1] * 0.72 + this.img[vx + 2] * 0.07);
19759
- }
19760
- const vol = frame4D * nx * ny * nz;
19761
- let i = this.img[vx + vol];
19762
- if (isReadImaginary) {
19763
- i = this.imaginary[vx + vol];
19764
- }
19765
- return this.hdr.scl_slope * i + this.hdr.scl_inter;
19766
- }
19767
20207
  /**
19768
- * @param id - id of 3D Object (is this the base volume or an overlay?)
19769
- * @param gl - WebGL rendering context
19770
- * @returns new 3D object in model space
20208
+ * Returns voxel intensity at specific native coordinates.
20209
+ * Delegates to VolumeUtils.getValue.
20210
+ * @param x - Native X coordinate (0-indexed)
20211
+ * @param y - Native Y coordinate (0-indexed)
20212
+ * @param z - Native Z coordinate (0-indexed)
20213
+ * @param frame4D - 4D frame index (0-indexed)
20214
+ * @param isReadImaginary - Flag to read from imaginary data array
20215
+ * @returns Scaled voxel intensity
19771
20216
  */
19772
- toNiivueObject3D(id, gl) {
19773
- const dimsRAS = this.dimsRAS;
19774
- const matRAS = this.matRAS;
19775
- const pixDimsRAS = this.pixDimsRAS;
19776
- const L = -0.5;
19777
- const P = -0.5;
19778
- const I = -0.5;
19779
- const R = dimsRAS[1] - 1 + 0.5;
19780
- const A = dimsRAS[2] - 1 + 0.5;
19781
- const S = dimsRAS[3] - 1 + 0.5;
19782
- const LPI = this.vox2mm([L, P, I], matRAS);
19783
- const LAI = this.vox2mm([L, A, I], matRAS);
19784
- const LPS = this.vox2mm([L, P, S], matRAS);
19785
- const LAS = this.vox2mm([L, A, S], matRAS);
19786
- const RPI = this.vox2mm([R, P, I], matRAS);
19787
- const RAI = this.vox2mm([R, A, I], matRAS);
19788
- const RPS = this.vox2mm([R, P, S], matRAS);
19789
- const RAS = this.vox2mm([R, A, S], matRAS);
19790
- const posTex = [
19791
- // spatial position (XYZ), texture coordinates UVW
19792
- // Superior face
19793
- ...LPS,
19794
- ...[0, 0, 1],
19795
- ...RPS,
19796
- ...[1, 0, 1],
19797
- ...RAS,
19798
- ...[1, 1, 1],
19799
- ...LAS,
19800
- ...[0, 1, 1],
19801
- // Inferior face
19802
- ...LPI,
19803
- ...[0, 0, 0],
19804
- ...LAI,
19805
- ...[0, 1, 0],
19806
- ...RAI,
19807
- ...[1, 1, 0],
19808
- ...RPI,
19809
- ...[1, 0, 0]
19810
- ];
19811
- const indexBuffer = gl.createBuffer();
19812
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
19813
- const indices = [
19814
- // six faces of cube: each has 2 triangles (6 indices)
19815
- 0,
19816
- 3,
19817
- 2,
19818
- 2,
19819
- 1,
19820
- 0,
19821
- // Top
19822
- 4,
19823
- 7,
19824
- 6,
19825
- 6,
19826
- 5,
19827
- 4,
19828
- // Bottom
19829
- 5,
19830
- 6,
19831
- 2,
19832
- 2,
19833
- 3,
19834
- 5,
19835
- // Front
19836
- 4,
19837
- 0,
19838
- 1,
19839
- 1,
19840
- 7,
19841
- 4,
19842
- // Back
19843
- 7,
19844
- 1,
19845
- 2,
19846
- 2,
19847
- 6,
19848
- 7,
19849
- // Right
19850
- 4,
19851
- 5,
19852
- 3,
19853
- 3,
19854
- 0,
19855
- 4
19856
- // Left
19857
- ];
19858
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
19859
- const posTexBuffer = gl.createBuffer();
19860
- gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
19861
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posTex), gl.STATIC_DRAW);
19862
- const vao = gl.createVertexArray();
19863
- gl.bindVertexArray(vao);
19864
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
19865
- gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
19866
- gl.enableVertexAttribArray(0);
19867
- gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
19868
- gl.enableVertexAttribArray(1);
19869
- gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
19870
- gl.bindVertexArray(null);
19871
- const obj3D = new NiivueObject3D(id, posTexBuffer, gl.TRIANGLES, indices.length, indexBuffer, vao);
19872
- const extents = getExtents([...LPS, ...RPS, ...RAS, ...LAS, ...LPI, ...LAI, ...RAI, ...RPI]);
19873
- obj3D.extentsMin = extents.min.slice();
19874
- obj3D.extentsMax = extents.max.slice();
19875
- obj3D.furthestVertexFromOrigin = extents.furthestVertexFromOrigin;
19876
- obj3D.originNegate = vec36.clone(extents.origin);
19877
- vec36.negate(obj3D.originNegate, obj3D.originNegate);
19878
- obj3D.fieldOfViewDeObliqueMM = [dimsRAS[1] * pixDimsRAS[1], dimsRAS[2] * pixDimsRAS[2], dimsRAS[3] * pixDimsRAS[3]];
19879
- return obj3D;
20217
+ getValue(x, y, z, frame4D = 0, isReadImaginary = false) {
20218
+ return getValue(this, x, y, z, frame4D, isReadImaginary);
19880
20219
  }
19881
20220
  /**
19882
20221
  * Update options for image
@@ -19920,89 +20259,18 @@ var NVImage = class _NVImage {
19920
20259
  return options;
19921
20260
  }
19922
20261
  /**
19923
- * Converts NVImage to NIfTI compliant byte array
20262
+ * Converts NVImage to NIfTI compliant byte array.
20263
+ * Handles potential re-orientation of drawing data.
20264
+ * Delegates to ImageWriter.toUint8Array.
20265
+ * @param drawingBytes - Optional Uint8Array drawing overlay
20266
+ * @returns Uint8Array
19924
20267
  */
19925
20268
  toUint8Array(drawingBytes = null) {
19926
- const isDrawing = drawingBytes !== null;
19927
- const hdrBytes = hdrToArrayBuffer({ ...this.hdr, vox_offset: 352 }, isDrawing);
19928
- let drawingBytesToBeConverted = drawingBytes;
19929
- if (isDrawing) {
19930
- const perm = this.permRAS;
19931
- if (perm[0] !== 1 || perm[1] !== 2 || perm[2] !== 3) {
19932
- const dims = this.hdr.dims;
19933
- const layout = [0, 0, 0];
19934
- for (let i = 0; i < 3; i++) {
19935
- for (let j2 = 0; j2 < 3; j2++) {
19936
- if (Math.abs(perm[i]) - 1 !== j2) {
19937
- continue;
19938
- }
19939
- layout[j2] = i * Math.sign(perm[i]);
19940
- }
19941
- }
19942
- let stride = 1;
19943
- const instride = [1, 1, 1];
19944
- const inflip = [false, false, false];
19945
- for (let i = 0; i < layout.length; i++) {
19946
- for (let j2 = 0; j2 < layout.length; j2++) {
19947
- const a = Math.abs(layout[j2]);
19948
- if (a !== i) {
19949
- continue;
19950
- }
19951
- instride[j2] = stride;
19952
- if (layout[j2] < 0 || Object.is(layout[j2], -0)) {
19953
- inflip[j2] = true;
19954
- }
19955
- stride *= dims[j2 + 1];
19956
- }
19957
- }
19958
- let xlut = NVUtilities.range(0, dims[1] - 1, 1);
19959
- if (inflip[0]) {
19960
- xlut = NVUtilities.range(dims[1] - 1, 0, -1);
19961
- }
19962
- for (let i = 0; i < dims[1]; i++) {
19963
- xlut[i] *= instride[0];
19964
- }
19965
- let ylut = NVUtilities.range(0, dims[2] - 1, 1);
19966
- if (inflip[1]) {
19967
- ylut = NVUtilities.range(dims[2] - 1, 0, -1);
19968
- }
19969
- for (let i = 0; i < dims[2]; i++) {
19970
- ylut[i] *= instride[1];
19971
- }
19972
- let zlut = NVUtilities.range(0, dims[3] - 1, 1);
19973
- if (inflip[2]) {
19974
- zlut = NVUtilities.range(dims[3] - 1, 0, -1);
19975
- }
19976
- for (let i = 0; i < dims[3]; i++) {
19977
- zlut[i] *= instride[2];
19978
- }
19979
- const inVs = new Uint8Array(drawingBytes);
19980
- const outVs = new Uint8Array(dims[1] * dims[2] * dims[3]);
19981
- let j = 0;
19982
- for (let z = 0; z < dims[3]; z++) {
19983
- for (let y = 0; y < dims[2]; y++) {
19984
- for (let x = 0; x < dims[1]; x++) {
19985
- outVs[j] = inVs[xlut[x] + ylut[y] + zlut[z]];
19986
- j++;
19987
- }
19988
- }
19989
- }
19990
- drawingBytesToBeConverted = outVs;
19991
- log.debug("drawing bytes");
19992
- log.debug(drawingBytesToBeConverted);
19993
- }
19994
- }
19995
- const img8 = isDrawing ? drawingBytesToBeConverted : new Uint8Array(this.img.buffer);
19996
- const opad = new Uint8Array(4);
19997
- const odata = new Uint8Array(hdrBytes.length + opad.length + img8.length);
19998
- odata.set(hdrBytes);
19999
- odata.set(opad, hdrBytes.length);
20000
- odata.set(img8, hdrBytes.length + opad.length);
20001
- return odata;
20269
+ return toUint8Array(this, drawingBytes);
20002
20270
  }
20003
20271
  // not included in public docs
20004
20272
  convertVox2Frac(vox) {
20005
- const frac = vec36.fromValues(
20273
+ const frac = vec38.fromValues(
20006
20274
  (vox[0] + 0.5) / this.dimsRAS[1],
20007
20275
  (vox[1] + 0.5) / this.dimsRAS[2],
20008
20276
  (vox[2] + 0.5) / this.dimsRAS[3]
@@ -20011,7 +20279,7 @@ var NVImage = class _NVImage {
20011
20279
  }
20012
20280
  // not included in public docs
20013
20281
  convertFrac2Vox(frac) {
20014
- const vox = vec36.fromValues(
20282
+ const vox = vec38.fromValues(
20015
20283
  Math.round(frac[0] * this.dims[1] - 0.5),
20016
20284
  // dims === RAS
20017
20285
  Math.round(frac[1] * this.dims[2] - 0.5),
@@ -20023,26 +20291,26 @@ var NVImage = class _NVImage {
20023
20291
  }
20024
20292
  // not included in public docs
20025
20293
  convertFrac2MM(frac, isForceSliceMM = false) {
20026
- const pos = vec44.fromValues(frac[0], frac[1], frac[2], 1);
20294
+ const pos = vec45.fromValues(frac[0], frac[1], frac[2], 1);
20027
20295
  if (isForceSliceMM) {
20028
- vec44.transformMat4(pos, pos, this.frac2mm);
20296
+ vec45.transformMat4(pos, pos, this.frac2mm);
20029
20297
  } else {
20030
- vec44.transformMat4(pos, pos, this.frac2mmOrtho);
20298
+ vec45.transformMat4(pos, pos, this.frac2mmOrtho);
20031
20299
  }
20032
20300
  return pos;
20033
20301
  }
20034
20302
  // not included in public docs
20035
20303
  convertMM2Frac(mm, isForceSliceMM = false) {
20036
- const mm4 = vec44.fromValues(mm[0], mm[1], mm[2], 1);
20304
+ const mm4 = vec45.fromValues(mm[0], mm[1], mm[2], 1);
20037
20305
  const d = this.dimsRAS;
20038
- const frac = vec36.fromValues(0, 0, 0);
20306
+ const frac = vec38.fromValues(0, 0, 0);
20039
20307
  if (typeof d === "undefined") {
20040
20308
  return frac;
20041
20309
  }
20042
20310
  if (!isForceSliceMM) {
20043
- const xform = mat44.clone(this.frac2mmOrtho);
20044
- mat44.invert(xform, xform);
20045
- vec44.transformMat4(mm4, mm4, xform);
20311
+ const xform = mat46.clone(this.frac2mmOrtho);
20312
+ mat46.invert(xform, xform);
20313
+ vec45.transformMat4(mm4, mm4, xform);
20046
20314
  frac[0] = mm4[0];
20047
20315
  frac[1] = mm4[1];
20048
20316
  frac[2] = mm4[2];
@@ -20051,10 +20319,10 @@ var NVImage = class _NVImage {
20051
20319
  if (d[1] < 1 || d[2] < 1 || d[3] < 1) {
20052
20320
  return frac;
20053
20321
  }
20054
- const sform = mat44.clone(this.matRAS);
20055
- mat44.invert(sform, sform);
20056
- mat44.transpose(sform, sform);
20057
- vec44.transformMat4(mm4, mm4, sform);
20322
+ const sform = mat46.clone(this.matRAS);
20323
+ mat46.invert(sform, sform);
20324
+ mat46.transpose(sform, sform);
20325
+ vec45.transformMat4(mm4, mm4, sform);
20058
20326
  frac[0] = (mm4[0] + 0.5) / d[1];
20059
20327
  frac[1] = (mm4[1] + 0.5) / d[2];
20060
20328
  frac[2] = (mm4[2] + 0.5) / d[3];
@@ -20223,17 +20491,18 @@ var DEFAULT_OPTIONS = {
20223
20491
  measureTextHeight: 0.03,
20224
20492
  isAlphaClipDark: false,
20225
20493
  gradientOrder: 1,
20226
- gradientOpacity: 0
20494
+ gradientOpacity: 0,
20495
+ invertScrollDirection: false
20227
20496
  };
20228
20497
  var INITIAL_SCENE_DATA = {
20229
20498
  gamma: 1,
20230
20499
  azimuth: 110,
20231
20500
  elevation: 10,
20232
- crosshairPos: vec37.fromValues(0.5, 0.5, 0.5),
20501
+ crosshairPos: vec39.fromValues(0.5, 0.5, 0.5),
20233
20502
  clipPlane: [0, 0, 0, 0],
20234
20503
  clipPlaneDepthAziElev: [2, 0, 0],
20235
20504
  volScaleMultiplier: 1,
20236
- pan2Dxyzmm: vec45.fromValues(0, 0, 0, 1),
20505
+ pan2Dxyzmm: vec46.fromValues(0, 0, 0, 1),
20237
20506
  clipThick: 2,
20238
20507
  clipVolumeLow: [0, 0, 0],
20239
20508
  clipVolumeHigh: [1, 1, 1]
@@ -20264,8 +20533,8 @@ var NVDocument = class _NVDocument {
20264
20533
  },
20265
20534
  sceneData: {
20266
20535
  ...INITIAL_SCENE_DATA,
20267
- pan2Dxyzmm: vec45.fromValues(0, 0, 0, 1),
20268
- crosshairPos: vec37.fromValues(0.5, 0.5, 0.5)
20536
+ pan2Dxyzmm: vec46.fromValues(0, 0, 0, 1),
20537
+ crosshairPos: vec39.fromValues(0.5, 0.5, 0.5)
20269
20538
  },
20270
20539
  get renderAzimuth() {
20271
20540
  return this.sceneData.azimuth;
@@ -20891,7 +21160,7 @@ var NVMesh3 = class _NVMesh {
20891
21160
  // given streamlines (which webGL renders as a single pixel), extrude to cylinders
20892
21161
  linesToCylinders(gl, posClrF32, indices) {
20893
21162
  function v4ToV3(v4) {
20894
- return vec38.fromValues(v4[0], v4[1], v4[2]);
21163
+ return vec310.fromValues(v4[0], v4[1], v4[2]);
20895
21164
  }
20896
21165
  const primitiveRestart = Math.pow(2, 32) - 1;
20897
21166
  const n_count = indices.length;
@@ -20913,11 +21182,11 @@ var NVMesh3 = class _NVMesh {
20913
21182
  const f32 = new Float32Array(npt * f32PerVertex);
20914
21183
  const u8 = new Uint8Array(f32.buffer);
20915
21184
  let vtx = 0;
20916
- let prevV4 = vec46.create();
20917
- let currV4 = vec46.create();
20918
- let nextV4 = vec46.create();
20919
- const v1 = vec38.create();
20920
- let prevV2 = vec38.create();
21185
+ let prevV4 = vec47.create();
21186
+ let currV4 = vec47.create();
21187
+ let nextV4 = vec47.create();
21188
+ const v1 = vec310.create();
21189
+ let prevV2 = vec310.create();
20921
21190
  let node = 0;
20922
21191
  const radius = this.fiberRadius;
20923
21192
  for (let i = 0; i < n_count; i++) {
@@ -20928,47 +21197,47 @@ var NVMesh3 = class _NVMesh {
20928
21197
  let idx2 = indices[i] * 4;
20929
21198
  node++;
20930
21199
  if (node <= 1) {
20931
- prevV4 = vec46.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
20932
- currV4 = vec46.clone(prevV4);
21200
+ prevV4 = vec47.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
21201
+ currV4 = vec47.clone(prevV4);
20933
21202
  if (i + 1 < n_count && indices[i + 1] !== primitiveRestart) {
20934
21203
  idx2 = indices[i + 1] * 4;
20935
- nextV4 = vec46.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
20936
- vec38.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4));
20937
- vec38.normalize(v1, v1);
21204
+ nextV4 = vec47.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
21205
+ vec310.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4));
21206
+ vec310.normalize(v1, v1);
20938
21207
  prevV2 = NiivueObject3D.getFirstPerpVector(v1);
20939
21208
  }
20940
21209
  continue;
20941
21210
  }
20942
21211
  if (isLineEnd) {
20943
- nextV4 = vec46.clone(currV4);
21212
+ nextV4 = vec47.clone(currV4);
20944
21213
  } else {
20945
- nextV4 = vec46.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
20946
- }
20947
- vec38.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4));
20948
- vec38.normalize(v1, v1);
20949
- const D = vec38.create();
20950
- vec38.cross(D, prevV2, v1);
20951
- const v2 = vec38.create();
20952
- vec38.cross(v2, v1, D);
20953
- prevV2 = vec38.clone(prevV2);
20954
- const v3 = vec38.create();
20955
- vec38.cross(v3, v1, v2);
20956
- vec38.normalize(v3, v3);
20957
- const vtxXYZ = vec38.create();
21214
+ nextV4 = vec47.fromValues(posClrF32[idx2 + 0], posClrF32[idx2 + 1], posClrF32[idx2 + 2], posClrF32[idx2 + 3]);
21215
+ }
21216
+ vec310.subtract(v1, v4ToV3(prevV4), v4ToV3(nextV4));
21217
+ vec310.normalize(v1, v1);
21218
+ const D = vec310.create();
21219
+ vec310.cross(D, prevV2, v1);
21220
+ const v2 = vec310.create();
21221
+ vec310.cross(v2, v1, D);
21222
+ prevV2 = vec310.clone(prevV2);
21223
+ const v3 = vec310.create();
21224
+ vec310.cross(v3, v1, v2);
21225
+ vec310.normalize(v3, v3);
21226
+ const vtxXYZ = vec310.create();
20958
21227
  for (let j = 0; j < cyl_sides; j++) {
20959
21228
  const c = Math.cos(j / cyl_sides * 2 * Math.PI);
20960
21229
  const s = Math.sin(j / cyl_sides * 2 * Math.PI);
20961
21230
  vtxXYZ[0] = radius * (c * v2[0] + s * v3[0]);
20962
21231
  vtxXYZ[1] = radius * (c * v2[1] + s * v3[1]);
20963
21232
  vtxXYZ[2] = radius * (c * v2[2] + s * v3[2]);
20964
- vec38.add(vtxXYZ, v4ToV3(currV4), vtxXYZ);
21233
+ vec310.add(vtxXYZ, v4ToV3(currV4), vtxXYZ);
20965
21234
  const fidx = vtx * f32PerVertex;
20966
21235
  f32[fidx + 0] = vtxXYZ[0];
20967
21236
  f32[fidx + 1] = vtxXYZ[1];
20968
21237
  f32[fidx + 2] = vtxXYZ[2];
20969
- const n3 = vec38.create();
20970
- vec38.subtract(n3, vtxXYZ, v4ToV3(currV4));
20971
- vec38.normalize(n3, n3);
21238
+ const n3 = vec310.create();
21239
+ vec310.subtract(n3, vtxXYZ, v4ToV3(currV4));
21240
+ vec310.normalize(n3, n3);
20972
21241
  const fidxU8 = (fidx + 3) * 4;
20973
21242
  u8[fidxU8 + 0] = n3[0] * 127;
20974
21243
  u8[fidxU8 + 1] = n3[1] * 127;
@@ -20976,8 +21245,8 @@ var NVMesh3 = class _NVMesh {
20976
21245
  f32[fidx + 4] = currV4[3];
20977
21246
  vtx++;
20978
21247
  }
20979
- prevV4 = vec46.clone(currV4);
20980
- currV4 = vec46.clone(nextV4);
21248
+ prevV4 = vec47.clone(currV4);
21249
+ currV4 = vec47.clone(nextV4);
20981
21250
  if (isLineEnd) {
20982
21251
  node = 0;
20983
21252
  }
@@ -21145,8 +21414,8 @@ var NVMesh3 = class _NVMesh {
21145
21414
  const vEnd3 = (offsetPt0[i + 1] - 1) * 3;
21146
21415
  let len = 0;
21147
21416
  for (let j = vStart3; j < vEnd3; j += 3) {
21148
- const v = vec38.fromValues(pts[j + 0] - pts[j + 3], pts[j + 1] - pts[j + 4], pts[j + 2] - pts[j + 5]);
21149
- len += vec38.len(v);
21417
+ const v = vec310.fromValues(pts[j + 0] - pts[j + 3], pts[j + 1] - pts[j + 4], pts[j + 2] - pts[j + 5]);
21418
+ len += vec310.len(v);
21150
21419
  }
21151
21420
  this.fiberLengths[i] = len;
21152
21421
  }
@@ -21173,8 +21442,8 @@ var NVMesh3 = class _NVMesh {
21173
21442
  return r + (g << 8) + (b << 16);
21174
21443
  }
21175
21444
  function direction2rgb(x1, y1, z1, x2, y2, z2, ditherFrac) {
21176
- const v = vec38.fromValues(Math.abs(x1 - x2), Math.abs(y1 - y2), Math.abs(z1 - z2));
21177
- vec38.normalize(v, v);
21445
+ const v = vec310.fromValues(Math.abs(x1 - x2), Math.abs(y1 - y2), Math.abs(z1 - z2));
21446
+ vec310.normalize(v, v);
21178
21447
  const r = ditherFrac - ditherHalf;
21179
21448
  for (let j = 0; j < 3; j++) {
21180
21449
  v[j] = 255 * Math.max(Math.min(Math.abs(v[j]) + r, 1), 0);
@@ -21820,9 +22089,14 @@ var NVMesh3 = class _NVMesh {
21820
22089
  // TODO this method is too generic
21821
22090
  setProperty(key, val, gl) {
21822
22091
  if (!(key in this)) {
21823
- log.warn("mesh does not have property ", key, this);
22092
+ console.warn("Mesh does not have property:", key, this);
21824
22093
  return;
21825
22094
  }
22095
+ if (typeof val !== "number" && typeof val !== "string" && typeof val !== "boolean") {
22096
+ console.warn("Invalid value type. Expected number, string, or boolean but received:", typeof val);
22097
+ return;
22098
+ }
22099
+ ;
21826
22100
  this[key] = val;
21827
22101
  this.updateMesh(gl);
21828
22102
  }
@@ -23849,7 +24123,7 @@ var NVLabel3D = class {
23849
24123
  };
23850
24124
 
23851
24125
  // src/nvconnectome.ts
23852
- import { vec3 as vec39 } from "gl-matrix";
24126
+ import { vec3 as vec311 } from "gl-matrix";
23853
24127
  var defaultOptions = {
23854
24128
  name: "untitled connectome",
23855
24129
  nodeColormap: "warm",
@@ -24142,7 +24416,7 @@ var NVConnectome = class _NVConnectome extends NVMesh3 {
24142
24416
  if (isNeg) {
24143
24417
  rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
24144
24418
  }
24145
- const pt = vec39.fromValues(nodes[i].x, nodes[i].y, nodes[i].z);
24419
+ const pt = vec311.fromValues(nodes[i].x, nodes[i].y, nodes[i].z);
24146
24420
  NiivueObject3D.makeColoredSphere(pts, tris, rgba255, radius, pt, rgba);
24147
24421
  }
24148
24422
  lut = cmapper.colormap(this.edgeColormap, this.colormapInvert);
@@ -24183,8 +24457,8 @@ var NVConnectome = class _NVConnectome extends NVMesh3 {
24183
24457
  if (isNeg) {
24184
24458
  rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
24185
24459
  }
24186
- const pti = vec39.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
24187
- const ptj = vec39.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
24460
+ const pti = vec311.fromValues(nodes[edge.first].x, nodes[edge.first].y, nodes[edge.first].z);
24461
+ const ptj = vec311.fromValues(nodes[edge.second].x, nodes[edge.second].y, nodes[edge.second].z);
24188
24462
  NiivueObject3D.makeColoredCylinder(pts, tris, rgba255, pti, ptj, radius, rgba);
24189
24463
  }
24190
24464
  }
@@ -24224,8 +24498,141 @@ var NVConnectome = class _NVConnectome extends NVMesh3 {
24224
24498
  }
24225
24499
  };
24226
24500
 
24501
+ // src/nvimage/RenderingUtils.ts
24502
+ import { vec3 as vec312 } from "gl-matrix";
24503
+ function toNiivueObject3D(nvImage, id, gl) {
24504
+ if (!nvImage.dimsRAS || !nvImage.matRAS || !nvImage.pixDimsRAS || !nvImage.vox2mm) {
24505
+ throw new Error("Cannot create NiivueObject3D: Missing required RAS properties or vox2mm access on NVImage.");
24506
+ }
24507
+ const dimsRAS = nvImage.dimsRAS;
24508
+ const matRAS = nvImage.matRAS;
24509
+ const pixDimsRAS = nvImage.pixDimsRAS;
24510
+ const L = -0.5;
24511
+ const P = -0.5;
24512
+ const I = -0.5;
24513
+ const R = dimsRAS[1] - 1 + 0.5;
24514
+ const A = dimsRAS[2] - 1 + 0.5;
24515
+ const S = dimsRAS[3] - 1 + 0.5;
24516
+ const vox2mmFn = nvImage.vox2mm;
24517
+ const LPI = vox2mmFn.call(nvImage, [L, P, I], matRAS);
24518
+ const LAI = vox2mmFn.call(nvImage, [L, A, I], matRAS);
24519
+ const LPS = vox2mmFn.call(nvImage, [L, P, S], matRAS);
24520
+ const LAS = vox2mmFn.call(nvImage, [L, A, S], matRAS);
24521
+ const RPI = vox2mmFn.call(nvImage, [R, P, I], matRAS);
24522
+ const RAI = vox2mmFn.call(nvImage, [R, A, I], matRAS);
24523
+ const RPS = vox2mmFn.call(nvImage, [R, P, S], matRAS);
24524
+ const RAS = vox2mmFn.call(nvImage, [R, A, S], matRAS);
24525
+ const posTex = [
24526
+ // Superior face vertices (Indices 0-3)
24527
+ ...LPS,
24528
+ ...[0, 0, 1],
24529
+ // 0
24530
+ ...RPS,
24531
+ ...[1, 0, 1],
24532
+ // 1
24533
+ ...RAS,
24534
+ ...[1, 1, 1],
24535
+ // 2
24536
+ ...LAS,
24537
+ ...[0, 1, 1],
24538
+ // 3
24539
+ // Inferior face vertices (Indices 4-7)
24540
+ ...LPI,
24541
+ ...[0, 0, 0],
24542
+ // 4
24543
+ ...LAI,
24544
+ ...[0, 1, 0],
24545
+ // 5
24546
+ ...RAI,
24547
+ ...[1, 1, 0],
24548
+ // 6
24549
+ ...RPI,
24550
+ ...[1, 0, 0]
24551
+ // 7
24552
+ ];
24553
+ const indexBuffer = gl.createBuffer();
24554
+ if (!indexBuffer) {
24555
+ throw new Error("Failed to create GL index buffer");
24556
+ }
24557
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
24558
+ const indices = [
24559
+ 0,
24560
+ 3,
24561
+ 2,
24562
+ 2,
24563
+ 1,
24564
+ 0,
24565
+ // Top
24566
+ 4,
24567
+ 7,
24568
+ 6,
24569
+ 6,
24570
+ 5,
24571
+ 4,
24572
+ // Bottom
24573
+ 5,
24574
+ 6,
24575
+ 2,
24576
+ 2,
24577
+ 3,
24578
+ 5,
24579
+ // Front -> Corresponds to LAI(5), RAI(6), RAS(2) / RAS(2), LAS(3), LAI(5)
24580
+ 4,
24581
+ 0,
24582
+ 1,
24583
+ 1,
24584
+ 7,
24585
+ 4,
24586
+ // Back -> Corresponds to LPI(4), LPS(0), RPS(1) / RPS(1), RPI(7), LPI(4)
24587
+ 7,
24588
+ 1,
24589
+ 2,
24590
+ 2,
24591
+ 6,
24592
+ 7,
24593
+ // Right -> Corresponds to RPI(7), RPS(1), RAS(2) / RAS(2), RAI(6), RPI(7)
24594
+ 4,
24595
+ 5,
24596
+ 3,
24597
+ 3,
24598
+ 0,
24599
+ 4
24600
+ // Left -> Corresponds to LPI(4), LAI(5), LAS(3) / LAS(3), LPS(0), LPI(4)
24601
+ ];
24602
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
24603
+ const posTexBuffer = gl.createBuffer();
24604
+ if (!posTexBuffer) {
24605
+ throw new Error("Failed to create GL vertex buffer");
24606
+ }
24607
+ gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
24608
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posTex), gl.STATIC_DRAW);
24609
+ const vao = gl.createVertexArray();
24610
+ if (!vao) {
24611
+ throw new Error("Failed to create GL VAO");
24612
+ }
24613
+ gl.bindVertexArray(vao);
24614
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
24615
+ gl.bindBuffer(gl.ARRAY_BUFFER, posTexBuffer);
24616
+ const stride = 24;
24617
+ gl.enableVertexAttribArray(0);
24618
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0);
24619
+ gl.enableVertexAttribArray(1);
24620
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, stride, 12);
24621
+ gl.bindVertexArray(null);
24622
+ const obj3D = new NiivueObject3D(id, posTexBuffer, gl.TRIANGLES, indices.length, indexBuffer, vao);
24623
+ const allCorners = [...LPS, ...RPS, ...RAS, ...LAS, ...LPI, ...LAI, ...RAI, ...RPI];
24624
+ const extents = getExtents(allCorners);
24625
+ obj3D.extentsMin = extents.min.slice();
24626
+ obj3D.extentsMax = extents.max.slice();
24627
+ obj3D.furthestVertexFromOrigin = extents.furthestVertexFromOrigin;
24628
+ obj3D.originNegate = vec312.clone(extents.origin);
24629
+ vec312.negate(obj3D.originNegate, obj3D.originNegate);
24630
+ obj3D.fieldOfViewDeObliqueMM = [dimsRAS[1] * pixDimsRAS[1], dimsRAS[2] * pixDimsRAS[2], dimsRAS[3] * pixDimsRAS[3]];
24631
+ return obj3D;
24632
+ }
24633
+
24227
24634
  // src/niivue/utils.ts
24228
- import { mat4 as mat45, vec3 as vec310, vec4 as vec47 } from "gl-matrix";
24635
+ import { mat4 as mat48, vec3 as vec313, vec4 as vec48 } from "gl-matrix";
24229
24636
  function readFileAsDataURL(input) {
24230
24637
  return new Promise((resolve, reject) => {
24231
24638
  let filePromise;
@@ -24382,27 +24789,27 @@ function negMinMax(min, max, minNeg, maxNeg) {
24382
24789
  return [mn, mx];
24383
24790
  }
24384
24791
  function swizzleVec3(vec, order = [0, 1, 2]) {
24385
- const vout = vec310.create();
24792
+ const vout = vec313.create();
24386
24793
  vout[0] = vec[order[0]];
24387
24794
  vout[1] = vec[order[1]];
24388
24795
  vout[2] = vec[order[2]];
24389
24796
  return vout;
24390
24797
  }
24391
24798
  function isRadiological(mtx) {
24392
- const vRight = vec47.fromValues(1, 0, 0, 0);
24393
- const vRotated = vec47.create();
24394
- vec47.transformMat4(vRotated, vRight, mtx);
24799
+ const vRight = vec48.fromValues(1, 0, 0, 0);
24800
+ const vRotated = vec48.create();
24801
+ vec48.transformMat4(vRotated, vRight, mtx);
24395
24802
  return vRotated[0];
24396
24803
  }
24397
24804
  function unProject(winX, winY, winZ, mvpMatrix) {
24398
- const inp = vec47.fromValues(winX, winY, winZ, 1);
24399
- const finalMatrix = mat45.clone(mvpMatrix);
24400
- mat45.invert(finalMatrix, finalMatrix);
24805
+ const inp = vec48.fromValues(winX, winY, winZ, 1);
24806
+ const finalMatrix = mat48.clone(mvpMatrix);
24807
+ mat48.invert(finalMatrix, finalMatrix);
24401
24808
  inp[0] = inp[0] * 2 - 1;
24402
24809
  inp[1] = inp[1] * 2 - 1;
24403
24810
  inp[2] = inp[2] * 2 - 1;
24404
- const out = vec47.create();
24405
- vec47.transformMat4(out, inp, finalMatrix);
24811
+ const out = vec48.create();
24812
+ vec48.transformMat4(out, inp, finalMatrix);
24406
24813
  if (out[3] === 0) {
24407
24814
  return out;
24408
24815
  }
@@ -24674,6 +25081,8 @@ var Niivue = class {
24674
25081
  __publicField(this, "extentsMax");
24675
25082
  // ResizeObserver
24676
25083
  __publicField(this, "resizeObserver", null);
25084
+ __publicField(this, "resizeEventListener", null);
25085
+ __publicField(this, "canvasObserver", null);
24677
25086
  // syncOpts: Record<string, unknown> = {}
24678
25087
  __publicField(this, "syncOpts", {
24679
25088
  "3d": false,
@@ -25062,6 +25471,42 @@ var Niivue = class {
25062
25471
  set isAlphaClipDark(newVal) {
25063
25472
  this.document.opts.isAlphaClipDark = newVal;
25064
25473
  }
25474
+ /**
25475
+ * Clean up event listeners and observers
25476
+ * Call this when the Niivue instance is no longer needed.
25477
+ * This will be called when the canvas is detached from the DOM
25478
+ * @example niivue.cleanup();
25479
+ */
25480
+ cleanup() {
25481
+ if (this.resizeEventListener) {
25482
+ window.removeEventListener("resize", this.resizeEventListener);
25483
+ this.resizeEventListener = null;
25484
+ }
25485
+ if (this.resizeObserver) {
25486
+ this.resizeObserver.disconnect();
25487
+ this.resizeObserver = null;
25488
+ }
25489
+ if (this.canvasObserver) {
25490
+ this.canvasObserver.disconnect();
25491
+ this.canvasObserver = null;
25492
+ }
25493
+ if (this.canvas && this.opts.interactive) {
25494
+ this.canvas.removeEventListener("mousedown", this.mouseDownListener.bind(this));
25495
+ this.canvas.removeEventListener("mouseup", this.mouseUpListener.bind(this));
25496
+ this.canvas.removeEventListener("mousemove", this.mouseMoveListener.bind(this));
25497
+ this.canvas.removeEventListener("touchstart", this.touchStartListener.bind(this));
25498
+ this.canvas.removeEventListener("touchend", this.touchEndListener.bind(this));
25499
+ this.canvas.removeEventListener("touchmove", this.touchMoveListener.bind(this));
25500
+ this.canvas.removeEventListener("wheel", this.wheelListener.bind(this));
25501
+ this.canvas.removeEventListener("contextmenu", this.mouseContextMenuListener.bind(this));
25502
+ this.canvas.removeEventListener("dblclick", this.resetBriCon.bind(this));
25503
+ this.canvas.removeEventListener("dragenter", this.dragEnterListener.bind(this));
25504
+ this.canvas.removeEventListener("dragover", this.dragOverListener.bind(this));
25505
+ this.canvas.removeEventListener("drop", this.dropListener.bind(this));
25506
+ this.canvas.removeEventListener("keyup", this.keyUpListener.bind(this));
25507
+ this.canvas.removeEventListener("keydown", this.keyDownListener.bind(this));
25508
+ }
25509
+ }
25065
25510
  get volumes() {
25066
25511
  return this.document.volumes;
25067
25512
  }
@@ -25124,7 +25569,7 @@ var Niivue = class {
25124
25569
  * @param id - the id of an html canvas element
25125
25570
  * @param isAntiAlias - determines if anti-aliasing is requested (if not specified, AA usage depends on hardware)
25126
25571
  * @example niivue = new Niivue().attachTo('gl')
25127
- * @example niivue.attachTo('gl')
25572
+ * @example await niivue.attachTo('gl')
25128
25573
  * @see {@link https://niivue.github.io/niivue/features/basic.multiplanar.html | live demo usage}
25129
25574
  */
25130
25575
  async attachTo(id, isAntiAlias = null) {
@@ -25137,7 +25582,7 @@ var Niivue = class {
25137
25582
  * @param canvas - the canvas element reference
25138
25583
  * @example
25139
25584
  * niivue = new Niivue()
25140
- * niivue.attachToCanvas(document.getElementById(id))
25585
+ * await niivue.attachToCanvas(document.getElementById(id))
25141
25586
  */
25142
25587
  async attachToCanvas(canvas, isAntiAlias = null) {
25143
25588
  this.canvas = canvas;
@@ -25157,17 +25602,27 @@ var Niivue = class {
25157
25602
  this.canvas.style.display = "block";
25158
25603
  this.canvas.width = this.canvas.offsetWidth;
25159
25604
  this.canvas.height = this.canvas.offsetHeight;
25160
- window.addEventListener("resize", () => {
25605
+ this.resizeEventListener = () => {
25161
25606
  requestAnimationFrame(() => {
25162
25607
  this.resizeListener();
25163
25608
  });
25164
- });
25609
+ };
25610
+ window.addEventListener("resize", this.resizeEventListener);
25165
25611
  this.resizeObserver = new ResizeObserver(() => {
25166
25612
  requestAnimationFrame(() => {
25167
25613
  this.resizeListener();
25168
25614
  });
25169
25615
  });
25170
25616
  this.resizeObserver.observe(this.canvas.parentElement);
25617
+ this.canvasObserver = new MutationObserver((mutations) => {
25618
+ for (const mutation of mutations) {
25619
+ if (mutation.type === "childList" && mutation.removedNodes.length > 0 && Array.from(mutation.removedNodes).includes(this.canvas)) {
25620
+ this.cleanup();
25621
+ break;
25622
+ }
25623
+ }
25624
+ });
25625
+ this.canvasObserver.observe(this.canvas.parentElement, { childList: true });
25171
25626
  }
25172
25627
  if (this.opts.interactive) {
25173
25628
  this.registerInteractions();
@@ -25220,7 +25675,7 @@ var Niivue = class {
25220
25675
  doSync2d(otherNV) {
25221
25676
  const thisMM = this.frac2mm(this.scene.crosshairPos);
25222
25677
  otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM);
25223
- otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
25678
+ otherNV.scene.pan2Dxyzmm = vec49.clone(this.scene.pan2Dxyzmm);
25224
25679
  }
25225
25680
  doSyncGamma(otherNV) {
25226
25681
  const thisGamma = this.scene.gamma;
@@ -25230,7 +25685,7 @@ var Niivue = class {
25230
25685
  }
25231
25686
  }
25232
25687
  doSyncZoomPan(otherNV) {
25233
- otherNV.scene.pan2Dxyzmm = vec48.clone(this.scene.pan2Dxyzmm);
25688
+ otherNV.scene.pan2Dxyzmm = vec49.clone(this.scene.pan2Dxyzmm);
25234
25689
  }
25235
25690
  doSyncCrosshair(otherNV) {
25236
25691
  const thisMM = this.frac2mm(this.scene.crosshairPos);
@@ -25467,7 +25922,7 @@ var Niivue = class {
25467
25922
  }
25468
25923
  this.setDragStart(pos.x, pos.y);
25469
25924
  if (!this.uiData.isDragging) {
25470
- this.uiData.pan2DxyzmmAtMouseDown = vec48.clone(this.scene.pan2Dxyzmm);
25925
+ this.uiData.pan2DxyzmmAtMouseDown = vec49.clone(this.scene.pan2Dxyzmm);
25471
25926
  }
25472
25927
  this.uiData.isDragging = true;
25473
25928
  this.uiData.dragClipPlaneStartDepthAziElev = this.scene.clipPlaneDepthAziElev;
@@ -25483,7 +25938,7 @@ var Niivue = class {
25483
25938
  }
25484
25939
  this.setDragStart(pos.x, pos.y);
25485
25940
  if (!this.uiData.isDragging) {
25486
- this.uiData.pan2DxyzmmAtMouseDown = vec48.clone(this.scene.pan2Dxyzmm);
25941
+ this.uiData.pan2DxyzmmAtMouseDown = vec49.clone(this.scene.pan2Dxyzmm);
25487
25942
  }
25488
25943
  this.uiData.isDragging = true;
25489
25944
  this.uiData.dragClipPlaneStartDepthAziElev = this.scene.clipPlaneDepthAziElev;
@@ -25574,9 +26029,9 @@ var Niivue = class {
25574
26029
  }
25575
26030
  const mmStart = this.frac2mm(fracStart);
25576
26031
  const mmEnd = this.frac2mm(fracEnd);
25577
- const v = vec311.create();
25578
- vec311.sub(v, vec311.fromValues(mmStart[0], mmStart[1], mmStart[2]), vec311.fromValues(mmEnd[0], mmEnd[1], mmEnd[2]));
25579
- const mmLength = vec311.len(v);
26032
+ const v = vec314.create();
26033
+ vec314.sub(v, vec314.fromValues(mmStart[0], mmStart[1], mmStart[2]), vec314.fromValues(mmEnd[0], mmEnd[1], mmEnd[2]));
26034
+ const mmLength = vec314.len(v);
25580
26035
  const voxStart = this.frac2vox(fracStart);
25581
26036
  const voxEnd = this.frac2vox(fracEnd);
25582
26037
  this.onDragRelease({
@@ -25613,16 +26068,7 @@ var Niivue = class {
25613
26068
  this.uiData.mouseButtonLeftDown = false;
25614
26069
  if (this.drawPenFillPts.length > 0) {
25615
26070
  this.drawPenFilled();
25616
- } else if (this.drawPenAxCorSag >= 0) {
25617
- if (this.opts.clickToSegment) {
25618
- const sumBitmap = this.sumBitmap(this.drawBitmap);
25619
- const sumSeg = this.sumBitmap(this.clickToSegmentGrowingBitmap);
25620
- if (sumSeg > sumBitmap) {
25621
- this.updateBitmapFromClickToSegment();
25622
- }
25623
- }
25624
26071
  }
25625
- this.drawAddUndoBitmap();
25626
26072
  this.drawPenLocation = [NaN, NaN, NaN];
25627
26073
  this.drawPenAxCorSag = -1;
25628
26074
  if (isFunction(this.onMouseUp)) {
@@ -25760,6 +26206,30 @@ var Niivue = class {
25760
26206
  this.uiData.windowY = y;
25761
26207
  }
25762
26208
  // not included in public docs
26209
+ // handler for mouse leaving the canvas
26210
+ mouseLeaveListener() {
26211
+ if (this.clickToSegmentIsGrowing) {
26212
+ log.debug("Mouse left canvas, stopping clickToSegment preview.");
26213
+ this.clickToSegmentIsGrowing = false;
26214
+ this.refreshDrawing(true, false);
26215
+ }
26216
+ if (this.opts.drawingEnabled && !isNaN(this.drawPenLocation[0])) {
26217
+ log.debug("Mouse left canvas during drawing, resetting pen state.");
26218
+ this.drawPenLocation = [NaN, NaN, NaN];
26219
+ this.drawPenAxCorSag = -1;
26220
+ this.drawPenFillPts = [];
26221
+ }
26222
+ if (this.uiData.isDragging) {
26223
+ log.debug("Mouse left canvas during drag, resetting drag state.");
26224
+ this.uiData.isDragging = false;
26225
+ this.uiData.mouseButtonLeftDown = false;
26226
+ this.uiData.mouseButtonCenterDown = false;
26227
+ this.uiData.mouseButtonRightDown = false;
26228
+ this.uiData.mousedown = false;
26229
+ this.drawScene();
26230
+ }
26231
+ }
26232
+ // not included in public docs
25763
26233
  // handler for mouse move over canvas
25764
26234
  // note: no test yet
25765
26235
  mouseMoveListener(e) {
@@ -25798,7 +26268,21 @@ var Niivue = class {
25798
26268
  const x = pos.x * this.uiData.dpr;
25799
26269
  const y = pos.y * this.uiData.dpr;
25800
26270
  this.mousePos = [x, y];
25801
- this.drawScene();
26271
+ const tileIdx = this.tileIndex(x, y);
26272
+ if (tileIdx >= 0 && this.opts.drawingEnabled) {
26273
+ const axCorSag = this.screenSlices[tileIdx].axCorSag;
26274
+ if (axCorSag <= 2 /* SAGITTAL */) {
26275
+ this.clickToSegmentXY = [x, y];
26276
+ this.clickToSegmentIsGrowing = true;
26277
+ this.doClickToSegment({
26278
+ x,
26279
+ // Screen X
26280
+ y,
26281
+ // Screen Y
26282
+ tileIndex: tileIdx
26283
+ });
26284
+ }
26285
+ }
25802
26286
  }
25803
26287
  }
25804
26288
  // not included in public docs
@@ -25869,7 +26353,7 @@ var Niivue = class {
25869
26353
  if (this.uiData.touchdown && e.touches.length < 2) {
25870
26354
  const rect = this.canvas.getBoundingClientRect();
25871
26355
  if (!this.uiData.isDragging) {
25872
- this.uiData.pan2DxyzmmAtMouseDown = vec48.clone(this.scene.pan2Dxyzmm);
26356
+ this.uiData.pan2DxyzmmAtMouseDown = vec49.clone(this.scene.pan2Dxyzmm);
25873
26357
  }
25874
26358
  this.uiData.isDragging = true;
25875
26359
  if (this.uiData.doubleTouch && this.uiData.isDragging) {
@@ -26017,22 +26501,65 @@ var Niivue = class {
26017
26501
  this.drawScene();
26018
26502
  this.uiData.isDragging = false;
26019
26503
  const tileIdx = this.tileIndex(this.uiData.dragStart[0], this.uiData.dragStart[1]);
26020
- this.generateMouseUpCallback(
26021
- this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
26022
- this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
26023
- );
26504
+ if (tileIdx >= 0) {
26505
+ this.generateMouseUpCallback(
26506
+ this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
26507
+ this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
26508
+ );
26509
+ } else {
26510
+ log.warn("Could not generate drag release callback for ROI selection: Invalid tile index.");
26511
+ }
26512
+ return;
26513
+ }
26514
+ let scrollAmount = e.deltaY < 0 ? -0.01 : 0.01;
26515
+ if (this.opts.invertScrollDirection) {
26516
+ scrollAmount = -scrollAmount;
26517
+ }
26518
+ if (this.opts.clickToSegment) {
26519
+ if (scrollAmount < 0) {
26520
+ this.opts.clickToSegmentPercent -= 0.01;
26521
+ this.opts.clickToSegmentPercent = Math.max(this.opts.clickToSegmentPercent, 0);
26522
+ } else {
26523
+ this.opts.clickToSegmentPercent += 0.01;
26524
+ this.opts.clickToSegmentPercent = Math.min(this.opts.clickToSegmentPercent, 1);
26525
+ }
26526
+ const x2 = this.clickToSegmentXY[0];
26527
+ const y2 = this.clickToSegmentXY[1];
26528
+ const tileIdx = this.tileIndex(x2, y2);
26529
+ if (tileIdx >= 0 && this.screenSlices[tileIdx].axCorSag <= 2 /* SAGITTAL */) {
26530
+ log.debug(`Adjusting clickToSegment threshold: ${this.opts.clickToSegmentPercent.toFixed(3)}`);
26531
+ this.clickToSegmentIsGrowing = true;
26532
+ this.doClickToSegment({ x: x2, y: y2, tileIndex: tileIdx });
26533
+ }
26024
26534
  return;
26025
26535
  }
26026
26536
  const rect = this.canvas.getBoundingClientRect();
26027
- if (e.deltaY < 0) {
26028
- this.sliceScroll2D(-0.01, e.clientX - rect.left, e.clientY - rect.top);
26029
- } else {
26030
- this.sliceScroll2D(0.01, e.clientX - rect.left, e.clientY - rect.top);
26537
+ const x = e.clientX - rect.left;
26538
+ const y = e.clientY - rect.top;
26539
+ if (this.opts.dragMode === 3 /* pan */ && this.inRenderTile(this.uiData.dpr * x, this.uiData.dpr * y) === -1) {
26540
+ const zoomDirection = scrollAmount < 0 ? 1 : -1;
26541
+ let zoom = this.scene.pan2Dxyzmm[3] * (1 + 10 * (0.01 * zoomDirection));
26542
+ zoom = Math.round(zoom * 10) / 10;
26543
+ const zoomChange = this.scene.pan2Dxyzmm[3] - zoom;
26544
+ if (this.opts.yoke3Dto2DZoom) {
26545
+ this.scene.volScaleMultiplier = zoom;
26546
+ }
26547
+ this.scene.pan2Dxyzmm[3] = zoom;
26548
+ const mm = this.frac2mm(this.scene.crosshairPos);
26549
+ this.scene.pan2Dxyzmm[0] += zoomChange * mm[0];
26550
+ this.scene.pan2Dxyzmm[1] += zoomChange * mm[1];
26551
+ this.scene.pan2Dxyzmm[2] += zoomChange * mm[2];
26552
+ this.drawScene();
26553
+ this.canvas.focus();
26554
+ this.sync();
26555
+ return;
26031
26556
  }
26557
+ this.sliceScroll2D(scrollAmount, x, y);
26032
26558
  }
26033
26559
  // not included in public docs
26034
26560
  // setup interactions with the canvas. Mouse clicks and touches
26035
26561
  // note: no test yet
26562
+ // note: any event listeners registered here should also be removed in `cleanup()`
26036
26563
  registerInteractions() {
26037
26564
  if (!this.canvas) {
26038
26565
  throw new Error("canvas undefined");
@@ -26040,6 +26567,7 @@ var Niivue = class {
26040
26567
  this.canvas.addEventListener("mousedown", this.mouseDownListener.bind(this));
26041
26568
  this.canvas.addEventListener("mouseup", this.mouseUpListener.bind(this));
26042
26569
  this.canvas.addEventListener("mousemove", this.mouseMoveListener.bind(this));
26570
+ this.canvas.addEventListener("mouseleave", this.mouseLeaveListener.bind(this));
26043
26571
  this.canvas.addEventListener("touchstart", this.touchStartListener.bind(this));
26044
26572
  this.canvas.addEventListener("touchend", this.touchEndListener.bind(this));
26045
26573
  this.canvas.addEventListener("touchmove", this.touchMoveListener.bind(this));
@@ -26048,7 +26576,13 @@ var Niivue = class {
26048
26576
  this.canvas.addEventListener("dblclick", this.resetBriCon.bind(this));
26049
26577
  this.canvas.addEventListener("dragenter", this.dragEnterListener.bind(this), false);
26050
26578
  this.canvas.addEventListener("dragover", this.dragOverListener.bind(this), false);
26051
- this.canvas.addEventListener("drop", this.dropListener.bind(this), false);
26579
+ this.canvas.addEventListener(
26580
+ "drop",
26581
+ (event) => {
26582
+ this.dropListener(event).catch(console.error);
26583
+ },
26584
+ false
26585
+ );
26052
26586
  this.canvas.setAttribute("tabindex", "0");
26053
26587
  this.canvas.addEventListener("keyup", this.keyUpListener.bind(this), false);
26054
26588
  this.canvas.addEventListener("keydown", this.keyDownListener.bind(this), false);
@@ -26284,9 +26818,9 @@ var Niivue = class {
26284
26818
  registers an external file loader for niivue to use when reading files.
26285
26819
 
26286
26820
  the loader must return an array buffer of the file contents for niivue
26287
- to parse and the extension of the file so niivue can infer the file type to load.
26821
+ to parse and the extension of the file so niivue can infer the file type to load.
26288
26822
 
26289
- example:
26823
+ example:
26290
26824
 
26291
26825
  const myCustomLoader = (File) => {
26292
26826
  // ... do parsing here ...
@@ -27578,6 +28112,14 @@ var Niivue = class {
27578
28112
  if (!this.drawBitmap) {
27579
28113
  this.createEmptyDrawing();
27580
28114
  }
28115
+ } else {
28116
+ if (this.clickToSegmentIsGrowing) {
28117
+ this.clickToSegmentIsGrowing = false;
28118
+ this.refreshDrawing(true, false);
28119
+ }
28120
+ this.drawPenLocation = [NaN, NaN, NaN];
28121
+ this.drawPenAxCorSag = -1;
28122
+ this.drawPenFillPts = [];
27581
28123
  }
27582
28124
  this.drawScene();
27583
28125
  }
@@ -27969,7 +28511,7 @@ var Niivue = class {
27969
28511
  }
27970
28512
 
27971
28513
  var nv1 = new Niivue();
27972
- nv1.attachTo("${canvasId}");
28514
+ await nv1.attachTo("${canvasId}");
27973
28515
  var base64 = "${base64}";
27974
28516
  NVUtilities.decompressBase64String(base64).then((jsonText) => {
27975
28517
  var json = JSON.parse(jsonText); // string -> JSON
@@ -28544,7 +29086,10 @@ var Niivue = class {
28544
29086
  }
28545
29087
  // not included in public docs
28546
29088
  // set color of single voxel in drawing
28547
- drawPt(x, y, z, penValue) {
29089
+ drawPt(x, y, z, penValue, drawBitmap = null) {
29090
+ if (drawBitmap === null) {
29091
+ drawBitmap = this.drawBitmap;
29092
+ }
28548
29093
  if (!this.back?.dims) {
28549
29094
  throw new Error("back.dims not set");
28550
29095
  }
@@ -28554,7 +29099,7 @@ var Niivue = class {
28554
29099
  x = Math.min(Math.max(x, 0), dx - 1);
28555
29100
  y = Math.min(Math.max(y, 0), dy - 1);
28556
29101
  z = Math.min(Math.max(z, 0), dz - 1);
28557
- this.drawBitmap[x + y * dx + z * dx * dy] = penValue;
29102
+ drawBitmap[x + y * dx + z * dx * dy] = penValue;
28558
29103
  const isAx = this.drawPenAxCorSag === 0;
28559
29104
  const isCor = this.drawPenAxCorSag === 1;
28560
29105
  const isSag = this.drawPenAxCorSag === 2;
@@ -28563,11 +29108,11 @@ var Niivue = class {
28563
29108
  for (let i = -halfPenSize; i <= halfPenSize; i++) {
28564
29109
  for (let j = -halfPenSize; j <= halfPenSize; j++) {
28565
29110
  if (isAx) {
28566
- this.drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
29111
+ drawBitmap[x + i + (y + j) * dx + z * dx * dy] = penValue;
28567
29112
  } else if (isCor) {
28568
- this.drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
29113
+ drawBitmap[x + i + y * dx + (z + j) * dx * dy] = penValue;
28569
29114
  } else if (isSag) {
28570
- this.drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
29115
+ drawBitmap[x + (y + j) * dx + (z + i) * dx * dy] = penValue;
28571
29116
  }
28572
29117
  }
28573
29118
  }
@@ -28826,9 +29371,34 @@ var Niivue = class {
28826
29371
  }
28827
29372
  // not included in public docs
28828
29373
  // set all connected voxels in drawing to new color
28829
- drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY, is2D = false) {
29374
+ drawFloodFill(seedXYZ, newColor = 0, growSelectedCluster = 0, forceMin = NaN, forceMax = NaN, neighbors = 6, maxDistanceMM = Number.POSITIVE_INFINITY, is2D = false, targetBitmap = null, isGrowClusterTool = false) {
28830
29375
  if (!this.drawBitmap) {
28831
- throw new Error("drawBitmap undefined");
29376
+ log.warn("drawFloodFill called without an initialized drawBitmap.");
29377
+ this.createEmptyDrawing();
29378
+ if (!this.drawBitmap) {
29379
+ log.error("Failed to create drawing bitmap.");
29380
+ return;
29381
+ }
29382
+ }
29383
+ if (this.clickToSegmentIsGrowing && !this.clickToSegmentGrowingBitmap) {
29384
+ log.warn("drawFloodFill called in preview mode without initialized clickToSegmentGrowingBitmap.");
29385
+ if (this.drawBitmap) {
29386
+ this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
29387
+ } else {
29388
+ log.error("Cannot initialize growing bitmap as drawBitmap is null.");
29389
+ return;
29390
+ }
29391
+ if (!this.clickToSegmentGrowingBitmap) {
29392
+ log.error("Failed to create growing bitmap.");
29393
+ return;
29394
+ }
29395
+ }
29396
+ if (targetBitmap === null) {
29397
+ targetBitmap = this.drawBitmap;
29398
+ }
29399
+ if (!targetBitmap) {
29400
+ log.error("drawFloodFill targetBitmap is null.");
29401
+ return;
28832
29402
  }
28833
29403
  if (!this.back?.dims) {
28834
29404
  throw new Error("back.dims undefined");
@@ -28844,15 +29414,12 @@ var Niivue = class {
28844
29414
  const nx = dims[0];
28845
29415
  const nxy = nx * dims[1];
28846
29416
  const nxyz = nxy * dims[2];
28847
- let img = this.drawBitmap.slice();
28848
- let drawBitmap = this.drawBitmap;
28849
- if (this.clickToSegmentIsGrowing) {
28850
- img = this.clickToSegmentGrowingBitmap.slice();
28851
- drawBitmap = this.clickToSegmentGrowingBitmap;
28852
- }
28853
- if (img.length !== nxy * dims[2]) {
29417
+ const originalBitmap = this.clickToSegmentIsGrowing ? this.drawBitmap : targetBitmap;
29418
+ if (!originalBitmap) {
29419
+ log.error("Could not determine original bitmap state.");
28854
29420
  return;
28855
29421
  }
29422
+ const img = new Uint8Array(nxyz).fill(0);
28856
29423
  let constrainXYZ = -1;
28857
29424
  if (is2D && this.drawPenAxCorSag === 0 /* AXIAL */) {
28858
29425
  constrainXYZ = 2;
@@ -28885,73 +29452,141 @@ var Niivue = class {
28885
29452
  return dist2 <= maxDistanceMM2;
28886
29453
  }
28887
29454
  const seedVx = xyz2vx(seedXYZ);
28888
- const seedColor = img[seedVx];
28889
- if (seedColor === newColor) {
28890
- if (growSelectedCluster !== 0) {
28891
- log.debug("drawFloodFill selected voxel is not part of a drawing");
28892
- } else {
28893
- log.debug("drawFloodFill selected voxel is already desired color");
29455
+ const originalSeedColor = originalBitmap[seedVx];
29456
+ if (isGrowClusterTool && originalSeedColor === 0) {
29457
+ log.debug("Grow/Erase Cluster tool requires starting on a masked voxel.");
29458
+ if (this.clickToSegmentIsGrowing && this.clickToSegmentGrowingBitmap && this.drawBitmap) {
29459
+ this.clickToSegmentGrowingBitmap.set(this.drawBitmap);
29460
+ this.refreshDrawing(true, true);
28894
29461
  }
28895
29462
  return;
28896
29463
  }
28897
- for (let i = 1; i < nxyz; i++) {
28898
- img[i] = 0;
28899
- if (drawBitmap[i] === seedColor) {
28900
- if (!isWithinDistance(i)) {
28901
- continue;
28902
- }
28903
- img[i] = 1;
29464
+ if (growSelectedCluster === 0 && originalSeedColor === newColor && !isGrowClusterTool && newColor !== 0) {
29465
+ log.debug("drawFloodFill selected voxel is already desired color");
29466
+ if (!this.clickToSegmentIsGrowing) {
29467
+ return;
28904
29468
  }
28905
29469
  }
28906
- this.drawFloodFillCore(img, seedVx, neighbors);
28907
- if (growSelectedCluster !== 0) {
29470
+ let baseIntensity = NaN;
29471
+ if (isGrowClusterTool && (growSelectedCluster === Number.POSITIVE_INFINITY || growSelectedCluster === Number.NEGATIVE_INFINITY)) {
29472
+ const tempImgForIdentification = originalBitmap.slice();
29473
+ for (let i = 0; i < nxyz; i++) {
29474
+ tempImgForIdentification[i] = tempImgForIdentification[i] === originalSeedColor && isWithinDistance(i) ? 1 : 0;
29475
+ }
29476
+ if (tempImgForIdentification[seedVx] !== 1) {
29477
+ log.error("Seed voxel could not be marked for cluster ID.");
29478
+ return;
29479
+ }
29480
+ this.drawFloodFillCore(tempImgForIdentification, seedVx, neighbors);
28908
29481
  const backImg = this.volumes[0].img2RAS();
28909
- let mx = backImg[seedVx];
28910
- let mn = mx;
28911
- if (isFinite(forceMax) && isFinite(forceMin)) {
28912
- mx = forceMax;
28913
- mn = forceMin;
29482
+ let clusterSum = 0;
29483
+ let clusterCount = 0;
29484
+ for (let i = 0; i < nxyz; i++) {
29485
+ if (tempImgForIdentification[i] === 2) {
29486
+ clusterSum += backImg[i];
29487
+ clusterCount++;
29488
+ }
29489
+ }
29490
+ baseIntensity = clusterCount > 0 ? clusterSum / clusterCount : backImg[seedVx];
29491
+ log.debug(`Grow Cluster using mean intensity: ${baseIntensity.toFixed(2)} from ${clusterCount} voxels.`);
29492
+ let fillMin = -Infinity;
29493
+ let fillMax = Infinity;
29494
+ if (growSelectedCluster === Number.POSITIVE_INFINITY) {
29495
+ fillMin = baseIntensity;
29496
+ }
29497
+ if (growSelectedCluster === Number.NEGATIVE_INFINITY) {
29498
+ fillMax = baseIntensity;
29499
+ }
29500
+ for (let i = 0; i < nxyz; i++) {
29501
+ if (tempImgForIdentification[i] === 2) {
29502
+ img[i] = 1;
29503
+ } else if (originalBitmap[i] === 0) {
29504
+ const intensity = backImg[i];
29505
+ if (intensity >= fillMin && intensity <= fillMax && isWithinDistance(i)) {
29506
+ img[i] = 1;
29507
+ }
29508
+ }
29509
+ }
29510
+ newColor = originalSeedColor;
29511
+ } else {
29512
+ if (growSelectedCluster === 0) {
29513
+ if (isGrowClusterTool && newColor === 0) {
29514
+ log.debug(`Erase Cluster: Identifying cluster with color ${originalSeedColor}`);
29515
+ for (let i = 0; i < nxyz; i++) {
29516
+ img[i] = originalBitmap[i] === originalSeedColor && isWithinDistance(i) ? 1 : 0;
29517
+ }
29518
+ } else {
29519
+ for (let i = 0; i < nxyz; i++) {
29520
+ if (originalBitmap[i] === originalSeedColor && isWithinDistance(i)) {
29521
+ if (originalSeedColor !== 0) {
29522
+ img[i] = 1;
29523
+ }
29524
+ }
29525
+ }
29526
+ }
28914
29527
  } else {
28915
- for (let i = 1; i < nxyz; i++) {
28916
- if (img[i] === 2) {
28917
- mx = Math.max(mx, backImg[i]);
28918
- mn = Math.min(mn, backImg[i]);
29528
+ const backImg = this.volumes[0].img2RAS();
29529
+ baseIntensity = backImg[seedVx];
29530
+ let fillMin = -Infinity;
29531
+ let fillMax = Infinity;
29532
+ if (isFinite(forceMin) && isFinite(forceMax)) {
29533
+ fillMin = forceMin;
29534
+ fillMax = forceMax;
29535
+ } else if (growSelectedCluster === Number.POSITIVE_INFINITY) {
29536
+ fillMin = baseIntensity;
29537
+ } else if (growSelectedCluster === Number.NEGATIVE_INFINITY) {
29538
+ fillMax = baseIntensity;
29539
+ }
29540
+ for (let i = 0; i < nxyz; i++) {
29541
+ const intensity = backImg[i];
29542
+ if (intensity >= fillMin && intensity <= fillMax && isWithinDistance(i)) {
29543
+ img[i] = 1;
28919
29544
  }
28920
29545
  }
28921
- if (growSelectedCluster === Number.POSITIVE_INFINITY) {
28922
- mx = growSelectedCluster;
29546
+ newColor = originalBitmap[seedVx];
29547
+ if (newColor === 0) {
29548
+ newColor = this.opts.penValue;
29549
+ if (newColor < 1 || !isFinite(newColor)) {
29550
+ newColor = 1;
29551
+ }
29552
+ }
29553
+ }
29554
+ }
29555
+ if (img[seedVx] !== 1) {
29556
+ let isSeedValidOriginal = false;
29557
+ if (isGrowClusterTool && growSelectedCluster !== 0) {
29558
+ if (originalSeedColor !== 0) {
29559
+ isSeedValidOriginal = true;
28923
29560
  }
28924
- if (growSelectedCluster === Number.NEGATIVE_INFINITY) {
28925
- mn = growSelectedCluster;
29561
+ } else {
29562
+ if (originalSeedColor !== 0 || newColor === 0) {
29563
+ isSeedValidOriginal = true;
28926
29564
  }
28927
29565
  }
28928
- log.debug("Intensity range of selected cluster :", mn, mx);
28929
- for (let i = 1; i < nxyz; i++) {
28930
- img[i] = 0;
28931
- if (backImg[i] >= mn && backImg[i] <= mx) {
28932
- if (!isWithinDistance(i)) {
28933
- continue;
28934
- }
28935
- img[i] = 1;
29566
+ if (isSeedValidOriginal && isWithinDistance(seedVx)) {
29567
+ img[seedVx] = 1;
29568
+ log.debug("Forcing seed voxel to 1 in working buffer.");
29569
+ } else {
29570
+ log.debug("Seed voxel not marked as candidate '1' and not valid originally.");
29571
+ if (this.clickToSegmentIsGrowing && this.clickToSegmentGrowingBitmap && this.drawBitmap) {
29572
+ this.clickToSegmentGrowingBitmap.set(this.drawBitmap);
28936
29573
  }
29574
+ return;
28937
29575
  }
28938
- this.drawFloodFillCore(img, seedVx, neighbors);
28939
- newColor = seedColor;
28940
29576
  }
28941
- for (let i = 1; i < nxyz; i++) {
29577
+ this.drawFloodFillCore(img, seedVx, neighbors);
29578
+ for (let i = 0; i < nxyz; i++) {
28942
29579
  if (img[i] === 2) {
28943
- drawBitmap[i] = newColor;
29580
+ targetBitmap[i] = newColor;
29581
+ } else if (this.clickToSegmentIsGrowing && targetBitmap === this.clickToSegmentGrowingBitmap) {
29582
+ targetBitmap[i] = originalBitmap[i];
28944
29583
  }
28945
29584
  }
28946
29585
  if (!this.clickToSegmentIsGrowing) {
28947
- this.drawBitmap = drawBitmap.slice();
28948
- if (!this.opts.clickToSegment) {
28949
- this.drawAddUndoBitmap();
28950
- }
28951
- this.refreshDrawing(true, this.clickToSegmentIsGrowing);
29586
+ this.drawAddUndoBitmap();
29587
+ this.refreshDrawing(true, false);
28952
29588
  } else {
28953
- this.clickToSegmentGrowingBitmap = drawBitmap;
28954
- this.refreshDrawing(true, this.clickToSegmentIsGrowing);
29589
+ this.refreshDrawing(true, true);
28955
29590
  }
28956
29591
  }
28957
29592
  // not included in public docs
@@ -29122,36 +29757,67 @@ var Niivue = class {
29122
29757
  * @see {@link https://niivue.github.io/niivue/features/cactus.html | live demo usage}
29123
29758
  */
29124
29759
  refreshDrawing(isForceRedraw = true, useClickToSegmentBitmap = false) {
29760
+ if (useClickToSegmentBitmap && (!this.opts.drawingEnabled || !this.opts.clickToSegment)) {
29761
+ log.debug("refreshDrawing: Conditions not met for clickToSegment bitmap, using drawBitmap.");
29762
+ useClickToSegmentBitmap = false;
29763
+ }
29764
+ const selectedBitmap = useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap;
29765
+ if (!selectedBitmap && !useClickToSegmentBitmap && this.clickToSegmentGrowingBitmap) {
29766
+ log.warn("refreshDrawing: drawBitmap is null, but clickToSegmentGrowingBitmap exists. Check state.");
29767
+ } else if (!selectedBitmap && useClickToSegmentBitmap && this.drawBitmap) {
29768
+ log.warn("refreshDrawing: clickToSegmentGrowingBitmap is null, falling back to drawBitmap.");
29769
+ useClickToSegmentBitmap = false;
29770
+ } else if (!selectedBitmap) {
29771
+ log.warn("refreshDrawing: Both bitmaps are null. Uploading empty data.");
29772
+ }
29773
+ const bitmapDataSource = useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap;
29125
29774
  if (!this.back?.dims) {
29126
- throw new Error("back.dims undefined");
29127
- }
29128
- if (!this.drawBitmap) {
29129
- throw new Error("drawBitmap undefined");
29775
+ log.warn("refreshDrawing: back.dims undefined, cannot refresh drawing texture yet.");
29776
+ return;
29130
29777
  }
29131
29778
  const dims = this.back.dims.slice();
29132
29779
  const vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
29133
- if (this.drawBitmap.length === 8) {
29780
+ if (!bitmapDataSource) {
29781
+ log.warn(
29782
+ `refreshDrawing: Bitmap data source (${useClickToSegmentBitmap ? "growing" : "main"}) is null. Cannot update texture.`
29783
+ );
29784
+ if (isForceRedraw) {
29785
+ this.drawScene();
29786
+ }
29787
+ return;
29788
+ }
29789
+ if (bitmapDataSource.length === 8) {
29134
29790
  dims[1] = 2;
29135
29791
  dims[2] = 2;
29136
29792
  dims[3] = 2;
29137
- } else if (vx !== this.drawBitmap.length) {
29138
- log.warn("Drawing bitmap must match the background image");
29793
+ } else if (vx !== bitmapDataSource.length) {
29794
+ log.warn(`Drawing bitmap length (${bitmapDataSource.length}) must match the background image (${vx})`);
29139
29795
  }
29140
29796
  this.gl.activeTexture(TEXTURE7_DRAW);
29141
29797
  this.gl.bindTexture(this.gl.TEXTURE_3D, this.drawTexture);
29142
- this.gl.texSubImage3D(
29143
- this.gl.TEXTURE_3D,
29144
- 0,
29145
- 0,
29146
- 0,
29147
- 0,
29148
- dims[1],
29149
- dims[2],
29150
- dims[3],
29151
- this.gl.RED,
29152
- this.gl.UNSIGNED_BYTE,
29153
- useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap
29154
- );
29798
+ if (!this.drawTexture) {
29799
+ log.error("refreshDrawing: drawTexture (GPU texture) is null.");
29800
+ return;
29801
+ }
29802
+ try {
29803
+ this.gl.texSubImage3D(
29804
+ this.gl.TEXTURE_3D,
29805
+ 0,
29806
+ 0,
29807
+ 0,
29808
+ 0,
29809
+ dims[1],
29810
+ dims[2],
29811
+ dims[3],
29812
+ this.gl.RED,
29813
+ this.gl.UNSIGNED_BYTE,
29814
+ bitmapDataSource
29815
+ // Use the determined source
29816
+ );
29817
+ } catch (error) {
29818
+ log.error("Error during texSubImage3D in refreshDrawing:", error);
29819
+ log.error("Dimensions:", dims, "Bitmap length:", bitmapDataSource?.length);
29820
+ }
29155
29821
  if (isForceRedraw) {
29156
29822
  this.drawScene();
29157
29823
  }
@@ -29989,10 +30655,10 @@ var Niivue = class {
29989
30655
  if (this.crosshairs3D) {
29990
30656
  this.crosshairs3D.mm[0] = NaN;
29991
30657
  }
29992
- let mtx = mat46.clone(overlayItem.toRAS);
30658
+ let mtx = mat49.clone(overlayItem.toRAS);
29993
30659
  if (layer === 0) {
29994
- this.volumeObject3D = overlayItem.toNiivueObject3D(this.VOLUME_ID, this.gl);
29995
- mat46.invert(mtx, mtx);
30660
+ this.volumeObject3D = toNiivueObject3D(overlayItem, this.VOLUME_ID, this.gl);
30661
+ mat49.invert(mtx, mtx);
29996
30662
  this.back.matRAS = overlayItem.matRAS;
29997
30663
  this.back.dims = overlayItem.dimsRAS;
29998
30664
  this.back.pixDims = overlayItem.pixDimsRAS;
@@ -30022,10 +30688,10 @@ var Niivue = class {
30022
30688
  let f100 = this.mm2frac(overlayItem.mm100, 0, true);
30023
30689
  let f010 = this.mm2frac(overlayItem.mm010, 0, true);
30024
30690
  let f001 = this.mm2frac(overlayItem.mm001, 0, true);
30025
- f100 = vec311.subtract(f100, f100, f000);
30026
- f010 = vec311.subtract(f010, f010, f000);
30027
- f001 = vec311.subtract(f001, f001, f000);
30028
- mtx = mat46.fromValues(
30691
+ f100 = vec314.subtract(f100, f100, f000);
30692
+ f010 = vec314.subtract(f010, f010, f000);
30693
+ f001 = vec314.subtract(f001, f001, f000);
30694
+ mtx = mat49.fromValues(
30029
30695
  f100[0],
30030
30696
  f010[0],
30031
30697
  f001[0],
@@ -30043,7 +30709,7 @@ var Niivue = class {
30043
30709
  0,
30044
30710
  1
30045
30711
  );
30046
- mat46.invert(mtx, mtx);
30712
+ mat49.invert(mtx, mtx);
30047
30713
  if (layer === 1) {
30048
30714
  outTexture = this.rgbaTex(this.overlayTexture, TEXTURE2_OVERLAY_VOL, this.back.dims);
30049
30715
  this.overlayTexture = outTexture;
@@ -30917,7 +31583,7 @@ var Niivue = class {
30917
31583
  // https://github.com/nipy/nibabel/blob/a2e5dee05cf374c22670ff9fd0d385ce366eb495/nibabel/freesurfer/mghformat.py#L30
30918
31584
  conformVox2Vox(inDims, inAffine, outDim = 256, outMM = 1, toRAS = false) {
30919
31585
  const a = inAffine.flat();
30920
- const affine = mat46.fromValues(
31586
+ const affine = mat49.fromValues(
30921
31587
  a[0],
30922
31588
  a[1],
30923
31589
  a[2],
@@ -30935,37 +31601,37 @@ var Niivue = class {
30935
31601
  a[14],
30936
31602
  a[15]
30937
31603
  );
30938
- const half = vec48.fromValues(inDims[1] / 2, inDims[2] / 2, inDims[3] / 2, 1);
30939
- const Pxyz_c4 = vec48.create();
30940
- const affineT = mat46.create();
30941
- mat46.transpose(affineT, affine);
30942
- vec48.transformMat4(Pxyz_c4, half, affineT);
30943
- const Pxyz_c = vec311.fromValues(Pxyz_c4[0], Pxyz_c4[1], Pxyz_c4[2]);
30944
- const delta = vec311.fromValues(outMM, outMM, outMM);
30945
- let Mdc = mat46.fromValues(-1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
31604
+ const half = vec49.fromValues(inDims[1] / 2, inDims[2] / 2, inDims[3] / 2, 1);
31605
+ const Pxyz_c4 = vec49.create();
31606
+ const affineT = mat49.create();
31607
+ mat49.transpose(affineT, affine);
31608
+ vec49.transformMat4(Pxyz_c4, half, affineT);
31609
+ const Pxyz_c = vec314.fromValues(Pxyz_c4[0], Pxyz_c4[1], Pxyz_c4[2]);
31610
+ const delta = vec314.fromValues(outMM, outMM, outMM);
31611
+ let Mdc = mat49.fromValues(-1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1);
30946
31612
  if (toRAS) {
30947
- Mdc = mat46.fromValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
30948
- }
30949
- mat46.transpose(Mdc, Mdc);
30950
- const dims = vec48.fromValues(outDim, outDim, outDim, 1);
30951
- const MdcD = mat46.create();
30952
- mat46.scale(MdcD, Mdc, delta);
30953
- const vol_center = vec48.fromValues(dims[0], dims[1], dims[2], 1);
30954
- vec48.transformMat4(vol_center, vol_center, MdcD);
30955
- vec48.scale(vol_center, vol_center, 0.5);
30956
- const translate = vec311.create();
30957
- vec311.subtract(translate, Pxyz_c, vec311.fromValues(vol_center[0], vol_center[1], vol_center[2]));
30958
- const out_affine = mat46.create();
30959
- mat46.transpose(out_affine, MdcD);
31613
+ Mdc = mat49.fromValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
31614
+ }
31615
+ mat49.transpose(Mdc, Mdc);
31616
+ const dims = vec49.fromValues(outDim, outDim, outDim, 1);
31617
+ const MdcD = mat49.create();
31618
+ mat49.scale(MdcD, Mdc, delta);
31619
+ const vol_center = vec49.fromValues(dims[0], dims[1], dims[2], 1);
31620
+ vec49.transformMat4(vol_center, vol_center, MdcD);
31621
+ vec49.scale(vol_center, vol_center, 0.5);
31622
+ const translate = vec314.create();
31623
+ vec314.subtract(translate, Pxyz_c, vec314.fromValues(vol_center[0], vol_center[1], vol_center[2]));
31624
+ const out_affine = mat49.create();
31625
+ mat49.transpose(out_affine, MdcD);
30960
31626
  out_affine[3] = translate[0];
30961
31627
  out_affine[7] = translate[1];
30962
31628
  out_affine[11] = translate[2];
30963
- const inv_out_affine = mat46.create();
30964
- mat46.invert(inv_out_affine, out_affine);
30965
- const vox2vox = mat46.create();
30966
- mat46.mul(vox2vox, affine, inv_out_affine);
30967
- const inv_vox2vox = mat46.create();
30968
- mat46.invert(inv_vox2vox, vox2vox);
31629
+ const inv_out_affine = mat49.create();
31630
+ mat49.invert(inv_out_affine, out_affine);
31631
+ const vox2vox = mat49.create();
31632
+ mat49.mul(vox2vox, affine, inv_out_affine);
31633
+ const inv_vox2vox = mat49.create();
31634
+ mat49.invert(inv_vox2vox, vox2vox);
30969
31635
  return [out_affine, vox2vox, inv_vox2vox];
30970
31636
  }
30971
31637
  // Create a binary byte array with a NIfTI format header as well as image data
@@ -31487,6 +32153,97 @@ var Niivue = class {
31487
32153
  }
31488
32154
  return sum;
31489
32155
  }
32156
+ doClickToSegment(options) {
32157
+ const { tileIndex } = options;
32158
+ if (tileIndex < 0 || tileIndex >= this.screenSlices.length) {
32159
+ log.warn(`Invalid tileIndex ${tileIndex} received in doClickToSegment.`);
32160
+ return;
32161
+ }
32162
+ const axCorSag = this.screenSlices[tileIndex].axCorSag;
32163
+ if (axCorSag > 2 /* SAGITTAL */) {
32164
+ log.warn("ClickToSegment attempted on non-2D slice tile.");
32165
+ return;
32166
+ }
32167
+ const texFrac = this.screenXY2TextureFrac(
32168
+ this.clickToSegmentXY[0],
32169
+ // Use the stored click location
32170
+ this.clickToSegmentXY[1],
32171
+ tileIndex,
32172
+ false
32173
+ );
32174
+ if (texFrac[0] < 0) {
32175
+ log.debug("Click location outside valid texture fraction for the tile.");
32176
+ return;
32177
+ }
32178
+ const pt = this.frac2vox(texFrac);
32179
+ const threshold = this.opts.clickToSegmentPercent;
32180
+ let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
32181
+ if (this.opts.clickToSegmentAutoIntensity) {
32182
+ if (threshold !== 0) {
32183
+ if (voxelIntensity === 0) {
32184
+ voxelIntensity = 0.01;
32185
+ }
32186
+ this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold);
32187
+ this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold);
32188
+ }
32189
+ if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
32190
+ this.opts.clickToSegmentBright = true;
32191
+ } else {
32192
+ this.opts.clickToSegmentBright = false;
32193
+ }
32194
+ }
32195
+ const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
32196
+ this.drawPenAxCorSag = axCorSag;
32197
+ const targetBitmap = this.clickToSegmentIsGrowing ? this.clickToSegmentGrowingBitmap : this.drawBitmap;
32198
+ if (!targetBitmap) {
32199
+ log.error("Target bitmap for flood fill is null.");
32200
+ if (!this.clickToSegmentIsGrowing) {
32201
+ this.createEmptyDrawing();
32202
+ if (!this.drawBitmap) {
32203
+ return;
32204
+ }
32205
+ } else {
32206
+ if (!this.drawBitmap) {
32207
+ this.createEmptyDrawing();
32208
+ }
32209
+ if (!this.drawBitmap) {
32210
+ return;
32211
+ }
32212
+ this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
32213
+ }
32214
+ log.warn("Initialized missing bitmap in doClickToSegment.");
32215
+ }
32216
+ this.drawFloodFill(
32217
+ [pt[0], pt[1], pt[2]],
32218
+ this.opts.penValue,
32219
+ brightOrDark,
32220
+ this.opts.clickToSegmentIntensityMin,
32221
+ this.opts.clickToSegmentIntensityMax,
32222
+ this.opts.floodFillNeighbors,
32223
+ this.opts.clickToSegmentMaxDistanceMM,
32224
+ this.opts.clickToSegmentIs2D,
32225
+ targetBitmap
32226
+ );
32227
+ if (!this.clickToSegmentIsGrowing) {
32228
+ log.debug("Applying clickToSegment mask to drawBitmap.");
32229
+ if (this.drawBitmap) {
32230
+ this.refreshDrawing(false, false);
32231
+ this.drawScene();
32232
+ } else {
32233
+ log.error("Cannot refresh drawing after click-to-segment apply, drawBitmap is null.");
32234
+ }
32235
+ if (this.drawBitmap) {
32236
+ const info = this.getDescriptives({
32237
+ layer: 0,
32238
+ masks: [],
32239
+ drawingIsMask: true
32240
+ // Use the final this.drawBitmap
32241
+ });
32242
+ this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
32243
+ }
32244
+ }
32245
+ this.createOnLocationChange(axCorSag);
32246
+ }
31490
32247
  // not included in public docs
31491
32248
  // handle mouse click event on canvas
31492
32249
  mouseClick(x, y, posChange = 0, isDelta = true) {
@@ -31533,138 +32290,94 @@ var Niivue = class {
31533
32290
  if (this.drawPenAxCorSag >= 0 && this.drawPenAxCorSag !== axCorSag) {
31534
32291
  continue;
31535
32292
  }
31536
- if (axCorSag > 2 /* SAGITTAL */) {
32293
+ if (axCorSag > 2 /* SAGITTAL */ && !this.opts.clickToSegment && posChange === 0) {
31537
32294
  continue;
31538
32295
  }
31539
- let texFrac = this.screenXY2TextureFrac(x, y, i, false);
32296
+ const texFrac = this.screenXY2TextureFrac(x, y, i, true);
31540
32297
  if (texFrac[0] < 0) {
31541
32298
  continue;
31542
32299
  }
31543
- if (!isDelta) {
31544
- this.scene.crosshairPos[2 - axCorSag] = posChange;
31545
- this.drawScene();
31546
- return;
31547
- }
31548
- if (posChange !== 0) {
31549
- let posNeg = 1;
31550
- if (posChange < 0) {
31551
- posNeg = -1;
32300
+ if (posChange !== 0 || !isDelta) {
32301
+ if (!isDelta) {
32302
+ if (axCorSag <= 2 /* SAGITTAL */) {
32303
+ this.scene.crosshairPos[2 - axCorSag] = posChange;
32304
+ this.drawScene();
32305
+ this.createOnLocationChange(axCorSag);
32306
+ }
32307
+ return;
31552
32308
  }
32309
+ const posNeg = posChange < 0 ? -1 : 1;
31553
32310
  const xyz = [0, 0, 0];
31554
- xyz[2 - axCorSag] = posNeg;
31555
- this.moveCrosshairInVox(xyz[0], xyz[1], xyz[2]);
31556
- this.drawScene();
31557
- this.createOnLocationChange(axCorSag);
32311
+ if (axCorSag <= 2 /* SAGITTAL */) {
32312
+ xyz[2 - axCorSag] = posNeg;
32313
+ this.moveCrosshairInVox(xyz[0], xyz[1], xyz[2]);
32314
+ }
31558
32315
  return;
31559
32316
  }
31560
32317
  if (this.opts.isForceMouseClickToVoxelCenters) {
31561
- this.scene.crosshairPos = vec311.clone(this.vox2frac(this.frac2vox(texFrac)));
32318
+ this.scene.crosshairPos = vec314.clone(this.vox2frac(this.frac2vox(texFrac)));
31562
32319
  } else {
31563
- this.scene.crosshairPos = vec311.clone(texFrac);
32320
+ this.scene.crosshairPos = vec314.clone(texFrac);
31564
32321
  }
31565
32322
  if (this.opts.drawingEnabled) {
31566
- let pt = this.frac2vox(this.scene.crosshairPos);
31567
- if (this.opts.clickToSegment) {
31568
- texFrac = this.screenXY2TextureFrac(this.clickToSegmentXY[0], this.clickToSegmentXY[1], i, false);
31569
- pt = this.frac2vox(texFrac);
31570
- let diff = 0;
31571
- let threshold = this.opts.clickToSegmentPercent + diff;
31572
- if (this.uiData.mousedown) {
31573
- const xDiff = (this.clickToSegmentXY[0] - x) / this.gl.canvas.width;
31574
- const yDiff = (this.clickToSegmentXY[1] - y) / this.gl.canvas.height;
31575
- diff = Math.max(Math.abs(xDiff), Math.abs(yDiff));
31576
- threshold = this.opts.clickToSegmentPercent + diff;
31577
- }
31578
- let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2]);
31579
- if (this.opts.clickToSegmentAutoIntensity) {
31580
- if (threshold !== 0) {
31581
- if (voxelIntensity === 0) {
31582
- voxelIntensity = 0.01;
31583
- }
31584
- this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold);
31585
- this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold);
31586
- }
31587
- if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
31588
- this.opts.clickToSegmentBright = true;
31589
- } else {
31590
- this.opts.clickToSegmentBright = false;
31591
- }
31592
- }
31593
- const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
31594
- this.drawPenAxCorSag = axCorSag;
31595
- if (this.drawPenAxCorSag === 0 /* AXIAL */) {
31596
- const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]];
31597
- const minPixDim = Math.min(...pixDims);
31598
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
31599
- } else if (this.drawPenAxCorSag === 1 /* CORONAL */) {
31600
- const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]];
31601
- const minPixDim = Math.min(...pixDims);
31602
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
31603
- } else if (this.drawPenAxCorSag === 2 /* SAGITTAL */) {
31604
- const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]];
31605
- const minPixDim = Math.min(...pixDims);
31606
- this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim);
31607
- }
31608
- this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue);
31609
- if (diff !== 0) {
31610
- this.clickToSegmentIsGrowing = true;
31611
- this.clickToSegmentGrowingBitmap = this.drawBitmap.slice();
32323
+ const pt = this.frac2vox(this.scene.crosshairPos);
32324
+ if (!isFinite(this.opts.penValue) || this.opts.penValue < 0 || Object.is(this.opts.penValue, -0)) {
32325
+ let growMode = 0;
32326
+ let floodFillNewColor = Math.abs(this.opts.penValue);
32327
+ const isGrowTool = true;
32328
+ if (Object.is(this.opts.penValue, -0)) {
32329
+ growMode = 0;
32330
+ floodFillNewColor = 0;
32331
+ log.debug("Erase Cluster selected");
31612
32332
  } else {
31613
- this.clickToSegmentIsGrowing = false;
32333
+ growMode = this.opts.penValue;
32334
+ log.debug("Intensity Grow selected", growMode);
31614
32335
  }
31615
32336
  this.drawFloodFill(
31616
- [pt[0], pt[1], pt[2]],
31617
- 0,
31618
- brightOrDark,
31619
- this.opts.clickToSegmentIntensityMin,
31620
- this.opts.clickToSegmentIntensityMax,
32337
+ pt,
32338
+ floodFillNewColor,
32339
+ growMode,
32340
+ NaN,
32341
+ NaN,
31621
32342
  this.opts.floodFillNeighbors,
31622
- this.opts.clickToSegmentMaxDistanceMM,
31623
- this.opts.clickToSegmentIs2D
32343
+ Number.POSITIVE_INFINITY,
32344
+ false,
32345
+ this.drawBitmap,
32346
+ isGrowTool
31624
32347
  );
31625
32348
  this.drawScene();
31626
32349
  this.createOnLocationChange(axCorSag);
31627
- if (this.clickToSegmentIsGrowing) {
31628
- return;
31629
- }
31630
- const info = this.getDescriptives({
31631
- layer: 0,
31632
- masks: [],
31633
- drawingIsMask: true
31634
- });
31635
- this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 });
31636
32350
  return;
31637
- }
31638
- if (!isFinite(this.opts.penValue) || this.opts.penValue < 0 || Object.is(this.opts.penValue, -0)) {
31639
- if (!isFinite(this.opts.penValue)) {
31640
- this.drawFloodFill(pt, 0, this.opts.penValue, NaN, NaN, this.opts.floodFillNeighbors);
31641
- } else {
31642
- this.drawFloodFill(
31643
- pt,
31644
- Math.abs(this.opts.penValue),
31645
- this.opts.penValue,
31646
- NaN,
31647
- NaN,
31648
- this.opts.floodFillNeighbors
31649
- );
32351
+ } else if (this.opts.clickToSegment) {
32352
+ if (axCorSag <= 2 /* SAGITTAL */) {
32353
+ this.clickToSegmentIsGrowing = false;
32354
+ this.doClickToSegment({
32355
+ x: this.clickToSegmentXY[0],
32356
+ y: this.clickToSegmentXY[1],
32357
+ tileIndex: i
32358
+ });
31650
32359
  }
32360
+ this.createOnLocationChange(axCorSag);
31651
32361
  return;
31652
- }
31653
- if (isNaN(this.drawPenLocation[0])) {
31654
- this.drawPenAxCorSag = axCorSag;
31655
- this.drawPenFillPts = [];
31656
- this.drawPt(...pt, this.opts.penValue);
31657
32362
  } else {
31658
- if (pt[0] === this.drawPenLocation[0] && pt[1] === this.drawPenLocation[1] && pt[2] === this.drawPenLocation[2]) {
31659
- return;
32363
+ if (isNaN(this.drawPenLocation[0])) {
32364
+ this.drawPenAxCorSag = axCorSag;
32365
+ this.drawPenFillPts = [];
32366
+ this.drawPt(...pt, this.opts.penValue);
32367
+ } else {
32368
+ if (pt[0] === this.drawPenLocation[0] && pt[1] === this.drawPenLocation[1] && pt[2] === this.drawPenLocation[2]) {
32369
+ this.drawScene();
32370
+ this.createOnLocationChange(axCorSag);
32371
+ return;
32372
+ }
32373
+ this.drawPenLine(pt, this.drawPenLocation, this.opts.penValue);
31660
32374
  }
31661
- this.drawPenLine(pt, this.drawPenLocation, this.opts.penValue);
31662
- }
31663
- this.drawPenLocation = pt;
31664
- if (this.opts.isFilledPen) {
31665
- this.drawPenFillPts.push(pt);
32375
+ this.drawPenLocation = pt;
32376
+ if (this.opts.isFilledPen) {
32377
+ this.drawPenFillPts.push(pt);
32378
+ }
32379
+ this.refreshDrawing(false, false);
31666
32380
  }
31667
- this.refreshDrawing(false);
31668
32381
  }
31669
32382
  this.drawScene();
31670
32383
  this.createOnLocationChange(axCorSag);
@@ -31755,9 +32468,9 @@ var Niivue = class {
31755
32468
  continue;
31756
32469
  }
31757
32470
  const mm = this.frac2mm(texFrac);
31758
- return vec48.fromValues(mm[0], mm[1], mm[2], i);
32471
+ return vec49.fromValues(mm[0], mm[1], mm[2], i);
31759
32472
  }
31760
- return vec48.fromValues(NaN, NaN, NaN, NaN);
32473
+ return vec49.fromValues(NaN, NaN, NaN, NaN);
31761
32474
  }
31762
32475
  // not included in public docs
31763
32476
  dragForPanZoom(startXYendXY) {
@@ -31769,9 +32482,9 @@ var Niivue = class {
31769
32482
  if (isNaN(startMM[0]) || isNaN(endMM[0]) || isNaN(endMM[3])) {
31770
32483
  return;
31771
32484
  }
31772
- const v = vec48.create();
32485
+ const v = vec49.create();
31773
32486
  const zoom = this.uiData.pan2DxyzmmAtMouseDown[3];
31774
- vec48.sub(v, endMM, startMM);
32487
+ vec49.sub(v, endMM, startMM);
31775
32488
  this.scene.pan2Dxyzmm[0] = this.uiData.pan2DxyzmmAtMouseDown[0] + zoom * v[0];
31776
32489
  this.scene.pan2Dxyzmm[1] = this.uiData.pan2DxyzmmAtMouseDown[1] + zoom * v[1];
31777
32490
  this.scene.pan2Dxyzmm[2] = this.uiData.pan2DxyzmmAtMouseDown[2] + zoom * v[2];
@@ -31848,12 +32561,12 @@ var Niivue = class {
31848
32561
  let endXY = this.canvasPos2frac([startXYendXY[2], startXYendXY[3]]);
31849
32562
  if (startXY[0] >= 0 && endXY[0] >= 0) {
31850
32563
  const startMm = this.frac2mm(startXY);
31851
- startXY = vec311.fromValues(startMm[0], startMm[1], startMm[2]);
32564
+ startXY = vec314.fromValues(startMm[0], startMm[1], startMm[2]);
31852
32565
  const endMm = this.frac2mm(endXY);
31853
- endXY = vec311.fromValues(endMm[0], endMm[1], endMm[2]);
31854
- const v = vec311.create();
31855
- vec311.sub(v, startXY, endXY);
31856
- const lenMM = vec311.len(v);
32566
+ endXY = vec314.fromValues(endMm[0], endMm[1], endMm[2]);
32567
+ const v = vec314.create();
32568
+ vec314.sub(v, startXY, endXY);
32569
+ const lenMM = vec314.len(v);
31857
32570
  let decimals = 2;
31858
32571
  if (lenMM > 9) {
31859
32572
  decimals = 1;
@@ -32393,7 +33106,7 @@ var Niivue = class {
32393
33106
  right = -mn[0];
32394
33107
  }
32395
33108
  const scale = 2 * Math.max(Math.abs(mn[2]), Math.abs(mx[2]));
32396
- const projectionMatrix = mat46.create();
33109
+ const projectionMatrix = mat49.create();
32397
33110
  let near = 0.01;
32398
33111
  let far = scale * 8;
32399
33112
  if (clipTolerance !== Infinity) {
@@ -32408,19 +33121,19 @@ var Niivue = class {
32408
33121
  near = dx - clipTolerance;
32409
33122
  far = dx + clipTolerance;
32410
33123
  }
32411
- mat46.ortho(projectionMatrix, left, right, mn[1], mx[1], near, far);
32412
- const modelMatrix = mat46.create();
33124
+ mat49.ortho(projectionMatrix, left, right, mn[1], mx[1], near, far);
33125
+ const modelMatrix = mat49.create();
32413
33126
  modelMatrix[0] = -1;
32414
- const translateVec3 = vec311.fromValues(0, 0, -scale * 1.8);
32415
- mat46.translate(modelMatrix, modelMatrix, translateVec3);
32416
- mat46.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
32417
- mat46.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
32418
- const iModelMatrix = mat46.create();
32419
- mat46.invert(iModelMatrix, modelMatrix);
32420
- const normalMatrix = mat46.create();
32421
- mat46.transpose(normalMatrix, iModelMatrix);
32422
- const modelViewProjectionMatrix = mat46.create();
32423
- mat46.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
33127
+ const translateVec3 = vec314.fromValues(0, 0, -scale * 1.8);
33128
+ mat49.translate(modelMatrix, modelMatrix, translateVec3);
33129
+ mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
33130
+ mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
33131
+ const iModelMatrix = mat49.create();
33132
+ mat49.invert(iModelMatrix, modelMatrix);
33133
+ const normalMatrix = mat49.create();
33134
+ mat49.transpose(normalMatrix, iModelMatrix);
33135
+ const modelViewProjectionMatrix = mat49.create();
33136
+ mat49.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
32424
33137
  return {
32425
33138
  modelViewProjectionMatrix,
32426
33139
  modelMatrix,
@@ -32440,7 +33153,7 @@ var Niivue = class {
32440
33153
  }
32441
33154
  // not included in public docs
32442
33155
  screenFieldOfViewVox(axCorSag = 0) {
32443
- const fov = vec311.clone(this.volumeObject3D.fieldOfViewDeObliqueMM);
33156
+ const fov = vec314.clone(this.volumeObject3D.fieldOfViewDeObliqueMM);
32444
33157
  return this.swizzleVec3MM(fov, axCorSag);
32445
33158
  }
32446
33159
  // not included in public docs
@@ -32451,25 +33164,25 @@ var Niivue = class {
32451
33164
  }
32452
33165
  const extentsMin = this.volumeObject3D.extentsMin;
32453
33166
  const extentsMax = this.volumeObject3D.extentsMax;
32454
- let mnMM = vec311.fromValues(extentsMin[0], extentsMin[1], extentsMin[2]);
32455
- let mxMM = vec311.fromValues(extentsMax[0], extentsMax[1], extentsMax[2]);
33167
+ let mnMM = vec314.fromValues(extentsMin[0], extentsMin[1], extentsMin[2]);
33168
+ let mxMM = vec314.fromValues(extentsMax[0], extentsMax[1], extentsMax[2]);
32456
33169
  mnMM = this.swizzleVec3MM(mnMM, axCorSag);
32457
33170
  mxMM = this.swizzleVec3MM(mxMM, axCorSag);
32458
- const fovMM = vec311.create();
32459
- vec311.subtract(fovMM, mxMM, mnMM);
33171
+ const fovMM = vec314.create();
33172
+ vec314.subtract(fovMM, mxMM, mnMM);
32460
33173
  return fovMM;
32461
33174
  }
32462
33175
  // not included in public docs
32463
33176
  screenFieldOfViewExtendedVox(axCorSag = 0) {
32464
33177
  const extentsMinOrtho = this.volumes[0].extentsMinOrtho;
32465
33178
  const extentsMaxOrtho = this.volumes[0].extentsMaxOrtho;
32466
- let mnMM = vec311.fromValues(extentsMinOrtho[0], extentsMinOrtho[1], extentsMinOrtho[2]);
32467
- let mxMM = vec311.fromValues(extentsMaxOrtho[0], extentsMaxOrtho[1], extentsMaxOrtho[2]);
32468
- const rotation = mat46.create();
33179
+ let mnMM = vec314.fromValues(extentsMinOrtho[0], extentsMinOrtho[1], extentsMinOrtho[2]);
33180
+ let mxMM = vec314.fromValues(extentsMaxOrtho[0], extentsMaxOrtho[1], extentsMaxOrtho[2]);
33181
+ const rotation = mat49.create();
32469
33182
  mnMM = this.swizzleVec3MM(mnMM, axCorSag);
32470
33183
  mxMM = this.swizzleVec3MM(mxMM, axCorSag);
32471
- const fovMM = vec311.create();
32472
- vec311.subtract(fovMM, mxMM, mnMM);
33184
+ const fovMM = vec314.create();
33185
+ vec314.subtract(fovMM, mxMM, mnMM);
32473
33186
  return { mnMM, mxMM, rotation, fovMM };
32474
33187
  }
32475
33188
  // not included in public docs
@@ -32479,13 +33192,13 @@ var Niivue = class {
32479
33192
  }
32480
33193
  const eMin = this.volumeObject3D.extentsMin;
32481
33194
  const eMax = this.volumeObject3D.extentsMax;
32482
- let mnMM = vec311.fromValues(eMin[0], eMin[1], eMin[2]);
32483
- let mxMM = vec311.fromValues(eMax[0], eMax[1], eMax[2]);
32484
- const rotation = mat46.create();
33195
+ let mnMM = vec314.fromValues(eMin[0], eMin[1], eMin[2]);
33196
+ let mxMM = vec314.fromValues(eMax[0], eMax[1], eMax[2]);
33197
+ const rotation = mat49.create();
32485
33198
  mnMM = this.swizzleVec3MM(mnMM, axCorSag);
32486
33199
  mxMM = this.swizzleVec3MM(mxMM, axCorSag);
32487
- const fovMM = vec311.create();
32488
- vec311.subtract(fovMM, mxMM, mnMM);
33200
+ const fovMM = vec314.create();
33201
+ vec314.subtract(fovMM, mxMM, mnMM);
32489
33202
  return { mnMM, mxMM, rotation, fovMM };
32490
33203
  }
32491
33204
  // not included in public docs
@@ -32549,9 +33262,9 @@ var Niivue = class {
32549
33262
  a = this.frac2mm(a);
32550
33263
  b = this.frac2mm(b);
32551
33264
  c = this.frac2mm(c);
32552
- a = this.swizzleVec3MM(vec311.fromValues(a[0], a[1], a[2]), axCorSag);
32553
- b = this.swizzleVec3MM(vec311.fromValues(b[0], b[1], b[2]), axCorSag);
32554
- c = this.swizzleVec3MM(vec311.fromValues(c[0], c[1], c[2]), axCorSag);
33265
+ a = this.swizzleVec3MM(vec314.fromValues(a[0], a[1], a[2]), axCorSag);
33266
+ b = this.swizzleVec3MM(vec314.fromValues(b[0], b[1], b[2]), axCorSag);
33267
+ c = this.swizzleVec3MM(vec314.fromValues(c[0], c[1], c[2]), axCorSag);
32555
33268
  const denom = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
32556
33269
  let yMult = (b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2]);
32557
33270
  yMult /= denom;
@@ -32570,10 +33283,10 @@ var Niivue = class {
32570
33283
  draw2DMain(leftTopWidthHeight, axCorSag, customMM = NaN) {
32571
33284
  let frac2mmTexture = this.volumes[0].frac2mm.slice();
32572
33285
  let screen = this.screenFieldOfViewExtendedMM(axCorSag);
32573
- let mesh2ortho = mat46.create();
33286
+ let mesh2ortho = mat49.create();
32574
33287
  if (!this.opts.isSliceMM) {
32575
33288
  frac2mmTexture = this.volumes[0].frac2mmOrtho.slice();
32576
- mesh2ortho = mat46.clone(this.volumes[0].mm2ortho);
33289
+ mesh2ortho = mat49.clone(this.volumes[0].mm2ortho);
32577
33290
  screen = this.screenFieldOfViewExtendedVox(axCorSag);
32578
33291
  }
32579
33292
  let isRadiolgical = this.opts.isRadiologicalConvention && axCorSag < 2 /* SAGITTAL */;
@@ -32616,7 +33329,7 @@ var Niivue = class {
32616
33329
  }
32617
33330
  if (isNaN(customMM)) {
32618
33331
  const pan = this.scene.pan2Dxyzmm;
32619
- const panXY = this.swizzleVec3MM(vec311.fromValues(pan[0], pan[1], pan[2]), axCorSag);
33332
+ const panXY = this.swizzleVec3MM(vec314.fromValues(pan[0], pan[1], pan[2]), axCorSag);
32620
33333
  const zoom = this.scene.pan2Dxyzmm[3];
32621
33334
  screen.mnMM[0] -= panXY[0];
32622
33335
  screen.mxMM[0] -= panXY[0];
@@ -32731,8 +33444,8 @@ var Niivue = class {
32731
33444
  isRadiolgical
32732
33445
  );
32733
33446
  }
32734
- const mx = mat46.clone(obj.modelViewProjectionMatrix);
32735
- mat46.multiply(mx, mx, mesh2ortho);
33447
+ const mx = mat49.clone(obj.modelViewProjectionMatrix);
33448
+ mat49.multiply(mx, mx, mesh2ortho);
32736
33449
  this.drawMesh3D(
32737
33450
  true,
32738
33451
  1,
@@ -32792,29 +33505,29 @@ var Niivue = class {
32792
33505
  const whratio = leftTopWidthHeight[2] / leftTopWidthHeight[3];
32793
33506
  let scale = this.furthestFromPivot;
32794
33507
  const origin = this.pivot3D;
32795
- const projectionMatrix = mat46.create();
33508
+ const projectionMatrix = mat49.create();
32796
33509
  scale = 0.8 * scale / this.scene.volScaleMultiplier;
32797
33510
  if (whratio < 1) {
32798
- mat46.ortho(projectionMatrix, -scale, scale, -scale / whratio, scale / whratio, scale * 0.01, scale * 8);
33511
+ mat49.ortho(projectionMatrix, -scale, scale, -scale / whratio, scale / whratio, scale * 0.01, scale * 8);
32799
33512
  } else {
32800
- mat46.ortho(projectionMatrix, -scale * whratio, scale * whratio, -scale, scale, scale * 0.01, scale * 8);
33513
+ mat49.ortho(projectionMatrix, -scale * whratio, scale * whratio, -scale, scale, scale * 0.01, scale * 8);
32801
33514
  }
32802
- const modelMatrix = mat46.create();
33515
+ const modelMatrix = mat49.create();
32803
33516
  modelMatrix[0] = -1;
32804
- const translateVec3 = vec311.fromValues(0, 0, -scale * 1.8);
32805
- mat46.translate(modelMatrix, modelMatrix, translateVec3);
33517
+ const translateVec3 = vec314.fromValues(0, 0, -scale * 1.8);
33518
+ mat49.translate(modelMatrix, modelMatrix, translateVec3);
32806
33519
  if (this.position) {
32807
- mat46.translate(modelMatrix, modelMatrix, this.position);
32808
- }
32809
- mat46.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
32810
- mat46.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
32811
- mat46.translate(modelMatrix, modelMatrix, [-origin[0], -origin[1], -origin[2]]);
32812
- const iModelMatrix = mat46.create();
32813
- mat46.invert(iModelMatrix, modelMatrix);
32814
- const normalMatrix = mat46.create();
32815
- mat46.transpose(normalMatrix, iModelMatrix);
32816
- const modelViewProjectionMatrix = mat46.create();
32817
- mat46.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
33520
+ mat49.translate(modelMatrix, modelMatrix, this.position);
33521
+ }
33522
+ mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
33523
+ mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
33524
+ mat49.translate(modelMatrix, modelMatrix, [-origin[0], -origin[1], -origin[2]]);
33525
+ const iModelMatrix = mat49.create();
33526
+ mat49.invert(iModelMatrix, modelMatrix);
33527
+ const normalMatrix = mat49.create();
33528
+ mat49.transpose(normalMatrix, iModelMatrix);
33529
+ const modelViewProjectionMatrix = mat49.create();
33530
+ mat49.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
32818
33531
  return [modelViewProjectionMatrix, modelMatrix, normalMatrix];
32819
33532
  }
32820
33533
  // not included in public docs
@@ -32822,13 +33535,13 @@ var Niivue = class {
32822
33535
  if (!this.back) {
32823
33536
  throw new Error("back undefined");
32824
33537
  }
32825
- const modelMatrix = mat46.create();
33538
+ const modelMatrix = mat49.create();
32826
33539
  modelMatrix[0] = -1;
32827
- mat46.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
32828
- mat46.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
33540
+ mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
33541
+ mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
32829
33542
  if (this.back.obliqueRAS) {
32830
- const oblique = mat46.clone(this.back.obliqueRAS);
32831
- mat46.multiply(modelMatrix, modelMatrix, oblique);
33543
+ const oblique = mat49.clone(this.back.obliqueRAS);
33544
+ mat49.multiply(modelMatrix, modelMatrix, oblique);
32832
33545
  }
32833
33546
  return modelMatrix;
32834
33547
  }
@@ -32836,15 +33549,15 @@ var Niivue = class {
32836
33549
  // calculate the near-far direction from the camera's perspective
32837
33550
  calculateRayDirection(azimuth, elevation) {
32838
33551
  const modelMatrix = this.calculateModelMatrix(azimuth, elevation);
32839
- const projectionMatrix = mat46.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1);
32840
- const mvpMatrix = mat46.create();
32841
- mat46.multiply(mvpMatrix, projectionMatrix, modelMatrix);
32842
- const inv = mat46.create();
32843
- mat46.invert(inv, mvpMatrix);
32844
- const rayDir4 = vec48.fromValues(0, 0, -1, 1);
32845
- vec48.transformMat4(rayDir4, rayDir4, inv);
32846
- const rayDir = vec311.fromValues(rayDir4[0], rayDir4[1], rayDir4[2]);
32847
- vec311.normalize(rayDir, rayDir);
33552
+ const projectionMatrix = mat49.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1);
33553
+ const mvpMatrix = mat49.create();
33554
+ mat49.multiply(mvpMatrix, projectionMatrix, modelMatrix);
33555
+ const inv = mat49.create();
33556
+ mat49.invert(inv, mvpMatrix);
33557
+ const rayDir4 = vec49.fromValues(0, 0, -1, 1);
33558
+ vec49.transformMat4(rayDir4, rayDir4, inv);
33559
+ const rayDir = vec314.fromValues(rayDir4[0], rayDir4[1], rayDir4[2]);
33560
+ vec314.normalize(rayDir, rayDir);
32848
33561
  const tiny = 5e-5;
32849
33562
  if (Math.abs(rayDir[0]) < tiny) {
32850
33563
  rayDir[0] = tiny;
@@ -32859,29 +33572,29 @@ var Niivue = class {
32859
33572
  }
32860
33573
  // not included in public docs
32861
33574
  sceneExtentsMinMax(isSliceMM = true) {
32862
- let mn = vec311.fromValues(0, 0, 0);
32863
- let mx = vec311.fromValues(0, 0, 0);
33575
+ let mn = vec314.fromValues(0, 0, 0);
33576
+ let mx = vec314.fromValues(0, 0, 0);
32864
33577
  if (this.volumes.length > 0) {
32865
33578
  if (!this.volumeObject3D) {
32866
33579
  throw new Error("volumeObject3D undefined");
32867
33580
  }
32868
- mn = vec311.fromValues(
33581
+ mn = vec314.fromValues(
32869
33582
  this.volumeObject3D.extentsMin[0],
32870
33583
  this.volumeObject3D.extentsMin[1],
32871
33584
  this.volumeObject3D.extentsMin[2]
32872
33585
  );
32873
- mx = vec311.fromValues(
33586
+ mx = vec314.fromValues(
32874
33587
  this.volumeObject3D.extentsMax[0],
32875
33588
  this.volumeObject3D.extentsMax[1],
32876
33589
  this.volumeObject3D.extentsMax[2]
32877
33590
  );
32878
33591
  if (!isSliceMM) {
32879
- mn = vec311.fromValues(
33592
+ mn = vec314.fromValues(
32880
33593
  this.volumes[0].extentsMinOrtho[0],
32881
33594
  this.volumes[0].extentsMinOrtho[1],
32882
33595
  this.volumes[0].extentsMinOrtho[2]
32883
33596
  );
32884
- mx = vec311.fromValues(
33597
+ mx = vec314.fromValues(
32885
33598
  this.volumes[0].extentsMaxOrtho[0],
32886
33599
  this.volumes[0].extentsMaxOrtho[1],
32887
33600
  this.volumes[0].extentsMaxOrtho[2]
@@ -32892,33 +33605,33 @@ var Niivue = class {
32892
33605
  if (this.volumes.length < 1) {
32893
33606
  const minExtents = this.meshes[0].extentsMin;
32894
33607
  const maxExtents = this.meshes[0].extentsMax;
32895
- mn = vec311.fromValues(minExtents[0], minExtents[1], minExtents[2]);
32896
- mx = vec311.fromValues(maxExtents[0], maxExtents[1], maxExtents[2]);
33608
+ mn = vec314.fromValues(minExtents[0], minExtents[1], minExtents[2]);
33609
+ mx = vec314.fromValues(maxExtents[0], maxExtents[1], maxExtents[2]);
32897
33610
  }
32898
33611
  for (let i = 0; i < this.meshes.length; i++) {
32899
33612
  const minExtents = this.meshes[i].extentsMin;
32900
33613
  const maxExtents = this.meshes[i].extentsMax;
32901
- const vmn = vec311.fromValues(minExtents[0], minExtents[1], minExtents[2]);
32902
- vec311.min(mn, mn, vmn);
32903
- const vmx = vec311.fromValues(maxExtents[0], maxExtents[1], maxExtents[2]);
32904
- vec311.max(mx, mx, vmx);
33614
+ const vmn = vec314.fromValues(minExtents[0], minExtents[1], minExtents[2]);
33615
+ vec314.min(mn, mn, vmn);
33616
+ const vmx = vec314.fromValues(maxExtents[0], maxExtents[1], maxExtents[2]);
33617
+ vec314.max(mx, mx, vmx);
32905
33618
  }
32906
33619
  }
32907
- const range = vec311.create();
32908
- vec311.subtract(range, mx, mn);
33620
+ const range = vec314.create();
33621
+ vec314.subtract(range, mx, mn);
32909
33622
  return [mn, mx, range];
32910
33623
  }
32911
33624
  // not included in public docs
32912
33625
  setPivot3D() {
32913
33626
  const [mn, mx] = this.sceneExtentsMinMax();
32914
- const pivot = vec311.create();
32915
- vec311.add(pivot, mn, mx);
32916
- vec311.scale(pivot, pivot, 0.5);
33627
+ const pivot = vec314.create();
33628
+ vec314.add(pivot, mn, mx);
33629
+ vec314.scale(pivot, pivot, 0.5);
32917
33630
  this.pivot3D = [pivot[0], pivot[1], pivot[2]];
32918
- vec311.subtract(pivot, mx, mn);
33631
+ vec314.subtract(pivot, mx, mn);
32919
33632
  this.extentsMin = mn;
32920
33633
  this.extentsMax = mx;
32921
- this.furthestFromPivot = vec311.length(pivot) * 0.5;
33634
+ this.furthestFromPivot = vec314.length(pivot) * 0.5;
32922
33635
  }
32923
33636
  // not included in public docs
32924
33637
  getMaxVols() {
@@ -33253,10 +33966,10 @@ var Niivue = class {
33253
33966
  gl.activeTexture(TEXTURE6_GRADIENT);
33254
33967
  gl.bindTexture(gl.TEXTURE_3D, this.gradientTexture);
33255
33968
  const modelMatrix = this.calculateModelMatrix(azimuth, elevation);
33256
- const iModelMatrix = mat46.create();
33257
- mat46.invert(iModelMatrix, modelMatrix);
33258
- const normalMatrix = mat46.create();
33259
- mat46.transpose(normalMatrix, iModelMatrix);
33969
+ const iModelMatrix = mat49.create();
33970
+ mat49.invert(iModelMatrix, modelMatrix);
33971
+ const normalMatrix = mat49.create();
33972
+ mat49.transpose(normalMatrix, iModelMatrix);
33260
33973
  gl.uniformMatrix4fv(shader.uniforms.normMtx, false, normalMatrix);
33261
33974
  }
33262
33975
  if (this.drawBitmap && this.drawBitmap.length > 8) {
@@ -33298,23 +34011,23 @@ var Niivue = class {
33298
34011
  gl.cullFace(gl.BACK);
33299
34012
  this.orientCubeShader.use(this.gl);
33300
34013
  gl.bindVertexArray(this.orientCubeShaderVAO);
33301
- const modelMatrix = mat46.create();
33302
- const projectionMatrix = mat46.create();
33303
- mat46.ortho(projectionMatrix, 0, gl.canvas.width, 0, gl.canvas.height, -10 * sz, 10 * sz);
34014
+ const modelMatrix = mat49.create();
34015
+ const projectionMatrix = mat49.create();
34016
+ mat49.ortho(projectionMatrix, 0, gl.canvas.width, 0, gl.canvas.height, -10 * sz, 10 * sz);
33304
34017
  let translateUpForColorbar = 0;
33305
34018
  if (leftTopWidthHeight[1] === 0) {
33306
34019
  translateUpForColorbar = gl.canvas.height - this.effectiveCanvasHeight();
33307
34020
  }
33308
- mat46.translate(modelMatrix, modelMatrix, [
34021
+ mat49.translate(modelMatrix, modelMatrix, [
33309
34022
  1.8 * sz + leftTopWidthHeight[0],
33310
34023
  translateUpForColorbar + 1.8 * sz + leftTopWidthHeight[1],
33311
34024
  0
33312
34025
  ]);
33313
- mat46.scale(modelMatrix, modelMatrix, [sz, sz, sz]);
33314
- mat46.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
33315
- mat46.rotateZ(modelMatrix, modelMatrix, deg2rad(-azimuth));
33316
- const modelViewProjectionMatrix = mat46.create();
33317
- mat46.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
34026
+ mat49.scale(modelMatrix, modelMatrix, [sz, sz, sz]);
34027
+ mat49.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
34028
+ mat49.rotateZ(modelMatrix, modelMatrix, deg2rad(-azimuth));
34029
+ const modelViewProjectionMatrix = mat49.create();
34030
+ mat49.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
33318
34031
  gl.uniformMatrix4fv(this.orientCubeShader.uniforms.u_matrix, false, modelViewProjectionMatrix);
33319
34032
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 168);
33320
34033
  gl.bindVertexArray(this.unusedVAO);
@@ -33417,8 +34130,8 @@ var Niivue = class {
33417
34130
  }
33418
34131
  // not included in public docs
33419
34132
  calculateScreenPoint(point, mvpMatrix, leftTopWidthHeight) {
33420
- const screenPoint = vec48.create();
33421
- vec48.transformMat4(screenPoint, [...point, 1], mvpMatrix);
34133
+ const screenPoint = vec49.create();
34134
+ vec49.transformMat4(screenPoint, [...point, 1], mvpMatrix);
33422
34135
  if (screenPoint[3] !== 0) {
33423
34136
  screenPoint[0] = (screenPoint[0] / screenPoint[3] + 1) * 0.5 * leftTopWidthHeight[2];
33424
34137
  screenPoint[1] = (1 - screenPoint[1] / screenPoint[3]) * 0.5 * leftTopWidthHeight[3];
@@ -33876,7 +34589,7 @@ var Niivue = class {
33876
34589
  // not included in public docs
33877
34590
  mm2frac(mm, volIdx = 0, isForceSliceMM = false) {
33878
34591
  if (this.volumes.length < 1) {
33879
- const frac = vec311.fromValues(0.1, 0.5, 0.5);
34592
+ const frac = vec314.fromValues(0.1, 0.5, 0.5);
33880
34593
  const [mn, _mx, range] = this.sceneExtentsMinMax();
33881
34594
  frac[0] = (mm[0] - mn[0]) / range[0];
33882
34595
  frac[1] = (mm[1] - mn[1]) / range[1];
@@ -33932,7 +34645,7 @@ var Niivue = class {
33932
34645
  }
33933
34646
  // not included in public docs
33934
34647
  frac2mm(frac, volIdx = 0, isForceSliceMM = false) {
33935
- const pos = vec48.fromValues(frac[0], frac[1], frac[2], 1);
34648
+ const pos = vec49.fromValues(frac[0], frac[1], frac[2], 1);
33936
34649
  if (this.volumes.length > 0) {
33937
34650
  return this.volumes[volIdx].convertFrac2MM(frac, isForceSliceMM || this.opts.isSliceMM);
33938
34651
  } else {
@@ -33946,7 +34659,7 @@ var Niivue = class {
33946
34659
  }
33947
34660
  // not included in public docs
33948
34661
  screenXY2TextureFrac(x, y, i, restrict0to1 = true) {
33949
- const texFrac = vec311.fromValues(-1, -1, -1);
34662
+ const texFrac = vec314.fromValues(-1, -1, -1);
33950
34663
  const axCorSag = this.screenSlices[i].axCorSag;
33951
34664
  if (axCorSag > 2 /* SAGITTAL */) {
33952
34665
  return texFrac;
@@ -33969,7 +34682,7 @@ var Niivue = class {
33969
34682
  if (this.screenSlices[i].AxyzMxy.length < 4) {
33970
34683
  return texFrac;
33971
34684
  }
33972
- let xyzMM = vec311.fromValues(0, 0, 0);
34685
+ let xyzMM = vec314.fromValues(0, 0, 0);
33973
34686
  xyzMM[0] = this.screenSlices[i].leftTopMM[0] + fracX * this.screenSlices[i].fovMM[0];
33974
34687
  xyzMM[1] = this.screenSlices[i].leftTopMM[1] + fracY * this.screenSlices[i].fovMM[1];
33975
34688
  const v = this.screenSlices[i].AxyzMxy;
@@ -34026,7 +34739,9 @@ var Niivue = class {
34026
34739
  h = this.gl.canvas.width / this.bmpTextureWH;
34027
34740
  w = this.gl.canvas.width;
34028
34741
  }
34029
- this.gl.uniform4f(this.bmpShader.uniforms.leftTopWidthHeight, 0, 0, w, h);
34742
+ const left = (this.gl.canvas.width - w) / 2;
34743
+ const top = (this.gl.canvas.height - h) / 2;
34744
+ this.gl.uniform4f(this.bmpShader.uniforms.leftTopWidthHeight, left, top, w, h);
34030
34745
  this.gl.bindVertexArray(this.genericVAO);
34031
34746
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
34032
34747
  this.gl.bindVertexArray(this.unusedVAO);
@@ -34722,23 +35437,6 @@ var Niivue = class {
34722
35437
  ]);
34723
35438
  return;
34724
35439
  }
34725
- if (this.opts.clickToSegment) {
34726
- const x = this.mousePos[0];
34727
- const y = this.mousePos[1];
34728
- if (this.inRenderTile(x, y) >= 0) {
34729
- return;
34730
- }
34731
- const tileIdx = this.tileIndex(x, y);
34732
- if (tileIdx > -1) {
34733
- const fovMM = this.screenSlices[tileIdx].fovMM;
34734
- const ltwh = this.screenSlices[tileIdx].leftTopWidthHeight;
34735
- const pixPerMM = ltwh[2] / fovMM[0];
34736
- const color = this.opts.crosshairColor;
34737
- const segmentCursorColor = [color[0], color[1], color[2], 0.4];
34738
- const radius = this.opts.clickToSegmentRadius * pixPerMM;
34739
- this.drawCircle([x - radius, y - radius, radius * 2, radius * 2], segmentCursorColor, 1);
34740
- }
34741
- }
34742
35440
  const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]]);
34743
35441
  posString = pos[0].toFixed(2) + "\xD7" + pos[1].toFixed(2) + "\xD7" + pos[2].toFixed(2);
34744
35442
  this.readyForSync = true;