@niivue/niivue 0.59.0 → 0.60.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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { mat4, vec3, vec4, vec2 } from 'gl-matrix';
2
- import { NIFTI1, NIFTI2 } from 'nifti-reader-js';
2
+ import { NIFTI1, NIFTI2, NIFTIEXTENSION } from 'nifti-reader-js';
3
3
 
4
4
  type ColorMap = {
5
5
  R: number[];
@@ -341,6 +341,7 @@ declare class NVImage {
341
341
  extentsMaxOrtho?: number[];
342
342
  mm2ortho?: mat4;
343
343
  hdr: NIFTI1 | NIFTI2 | null;
344
+ extensions?: NIFTIEXTENSION[];
344
345
  imageType?: ImageType;
345
346
  img?: TypedVoxelArray;
346
347
  imaginary?: Float32Array;
@@ -597,7 +598,8 @@ declare enum DRAG_MODE {
597
598
  pan = 3,
598
599
  slicer3D = 4,
599
600
  callbackOnly = 5,
600
- roiSelection = 6
601
+ roiSelection = 6,
602
+ angle = 7
601
603
  }
602
604
  declare enum DRAG_MODE_SECONDARY {
603
605
  none = 0,
@@ -606,7 +608,8 @@ declare enum DRAG_MODE_SECONDARY {
606
608
  pan = 3,
607
609
  slicer3D = 4,
608
610
  callbackOnly = 5,
609
- roiSelection = 6
611
+ roiSelection = 6,
612
+ angle = 7
610
613
  }
611
614
  declare enum DRAG_MODE_PRIMARY {
612
615
  crosshair = 0,
@@ -1568,6 +1571,8 @@ type UIData = {
1568
1571
  max3D?: number;
1569
1572
  windowX: number;
1570
1573
  windowY: number;
1574
+ angleFirstLine: number[];
1575
+ angleState: 'none' | 'drawing_first_line' | 'drawing_second_line' | 'complete';
1571
1576
  };
1572
1577
  type SaveImageOptions = {
1573
1578
  filename: string;
@@ -1720,6 +1725,7 @@ declare class Niivue {
1720
1725
  dragModes: {
1721
1726
  contrast: DRAG_MODE;
1722
1727
  measurement: DRAG_MODE;
1728
+ angle: DRAG_MODE;
1723
1729
  none: DRAG_MODE;
1724
1730
  pan: DRAG_MODE;
1725
1731
  slicer3D: DRAG_MODE;
@@ -3696,7 +3702,32 @@ declare class Niivue {
3696
3702
  * Draw a measurement line with end caps and length text on a 2D tile.
3697
3703
  * @internal
3698
3704
  */
3699
- drawMeasurementTool(startXYendXY: number[]): void;
3705
+ drawMeasurementTool(startXYendXY: number[], isDrawText?: boolean): void;
3706
+ /**
3707
+ * Draw angle measurement tool with two lines and angle display.
3708
+ * @internal
3709
+ */
3710
+ drawAngleMeasurementTool(): void;
3711
+ /**
3712
+ * Calculate and draw angle text at the intersection of two lines.
3713
+ * @internal
3714
+ */
3715
+ drawAngleText(): void;
3716
+ /**
3717
+ * Calculate angle between two lines in degrees.
3718
+ * @internal
3719
+ */
3720
+ calculateAngleBetweenLines(line1: number[], line2: number[]): number;
3721
+ /**
3722
+ * Reset the angle measurement state.
3723
+ * @internal
3724
+ */
3725
+ resetAngleMeasurement(): void;
3726
+ /**
3727
+ * Set the drag mode for mouse interactions.
3728
+ * @param mode - The drag mode to set ('none', 'contrast', 'measurement', 'angle', 'pan', 'slicer3D', 'callbackOnly', 'roiSelection')
3729
+ */
3730
+ setDragMode(mode: string | DRAG_MODE): void;
3700
3731
  /**
3701
3732
  * Draw a rectangle or outline at given position with specified color or default crosshair color.
3702
3733
  * @internal
package/build/index.js CHANGED
@@ -19,7 +19,7 @@ import { mat4 as mat49, vec2, vec3 as vec314, vec4 as vec410 } from "gl-matrix";
19
19
  // package.json
20
20
  var package_default = {
21
21
  name: "@niivue/niivue",
22
- version: "0.59.0",
22
+ version: "0.60.0",
23
23
  description: "minimal webgl2 nifti image viewer",
24
24
  types: "./build/index.d.ts",
25
25
  main: "./build/index.js",
@@ -16245,7 +16245,7 @@ function hdrToArrayBuffer(hdr, isDrawing8 = false, isInputEndian = false) {
16245
16245
  view.setFloat32(112, 1, isLittleEndian);
16246
16246
  view.setFloat32(116, 0, isLittleEndian);
16247
16247
  } else {
16248
- view.setFloat32(108, 352, isLittleEndian);
16248
+ view.setFloat32(108, hdr.vox_offset, isLittleEndian);
16249
16249
  view.setFloat32(112, hdr.scl_slope, isLittleEndian);
16250
16250
  view.setFloat32(116, hdr.scl_inter, isLittleEndian);
16251
16251
  }
@@ -16481,7 +16481,32 @@ function toUint8Array(nvImage, drawingBytes = null) {
16481
16481
  }
16482
16482
  const isDrawing = drawingBytes !== null;
16483
16483
  const hdrCopy = JSON.parse(JSON.stringify(nvImage.hdr));
16484
- hdrCopy.vox_offset = Math.max(352, hdrCopy.vox_offset);
16484
+ const hasExtensions = nvImage.extensions && nvImage.extensions.length > 0;
16485
+ const extFlag = new Uint8Array(4);
16486
+ extFlag[0] = hasExtensions ? 1 : 0;
16487
+ let extensionsData = new Uint8Array(0);
16488
+ if (hasExtensions) {
16489
+ const blocks = [];
16490
+ let totalSize = 0;
16491
+ for (const ext of nvImage.extensions) {
16492
+ const edataBytes = new Uint8Array(ext.edata);
16493
+ const block = new Uint8Array(8 + edataBytes.length);
16494
+ const dv = new DataView(block.buffer);
16495
+ dv.setInt32(0, ext.esize, true);
16496
+ dv.setInt32(4, ext.ecode, true);
16497
+ block.set(edataBytes, 8);
16498
+ blocks.push(block);
16499
+ totalSize += block.length;
16500
+ }
16501
+ extensionsData = new Uint8Array(totalSize);
16502
+ let offset2 = 0;
16503
+ for (const block of blocks) {
16504
+ extensionsData.set(block, offset2);
16505
+ offset2 += block.length;
16506
+ }
16507
+ }
16508
+ const headerSize = 348;
16509
+ hdrCopy.vox_offset = Math.max(352, headerSize + extFlag.length + extensionsData.length);
16485
16510
  if (isDrawing) {
16486
16511
  hdrCopy.datatypeCode = 2 /* DT_UINT8 */;
16487
16512
  hdrCopy.numBitsPerVoxel = 8;
@@ -16545,13 +16570,20 @@ function toUint8Array(nvImage, drawingBytes = null) {
16545
16570
  }
16546
16571
  imageBytesToSave = new Uint8Array(nvImage.img.buffer, nvImage.img.byteOffset, nvImage.img.byteLength);
16547
16572
  }
16548
- const headerSize = hdrBytes.length;
16549
- const paddingSize = Math.max(0, hdrCopy.vox_offset - headerSize);
16573
+ const preImageBytesSize = hdrBytes.length + extFlag.length + extensionsData.length;
16574
+ const paddingSize = Math.max(0, hdrCopy.vox_offset - preImageBytesSize);
16550
16575
  const padding = new Uint8Array(paddingSize);
16551
16576
  const totalLength = hdrCopy.vox_offset + imageBytesToSave.length;
16552
16577
  const outputData = new Uint8Array(totalLength);
16553
- outputData.set(hdrBytes, 0);
16554
- outputData.set(padding, headerSize);
16578
+ let offset = 0;
16579
+ outputData.set(hdrBytes, offset);
16580
+ offset += hdrBytes.length;
16581
+ outputData.set(extFlag, offset);
16582
+ offset += extFlag.length;
16583
+ outputData.set(extensionsData, offset);
16584
+ offset += extensionsData.length;
16585
+ outputData.set(padding, offset);
16586
+ offset += padding.length;
16555
16587
  outputData.set(imageBytesToSave, hdrCopy.vox_offset);
16556
16588
  return outputData;
16557
16589
  }
@@ -16994,7 +17026,7 @@ var nii_exports = {};
16994
17026
  __export(nii_exports, {
16995
17027
  readNifti: () => readNifti
16996
17028
  });
16997
- import { isCompressed, decompressAsync, readHeaderAsync, readImage } from "nifti-reader-js";
17029
+ import { isCompressed, decompressAsync, readHeaderAsync, readImage, hasExtension } from "nifti-reader-js";
16998
17030
  async function readNifti(nvImage, buffer) {
16999
17031
  let dataBuffer = buffer;
17000
17032
  let imgRaw = null;
@@ -17008,6 +17040,9 @@ async function readNifti(nvImage, buffer) {
17008
17040
  throw new Error("Buffer became invalid after decompression attempt.");
17009
17041
  }
17010
17042
  nvImage.hdr = await readHeaderAsync(dataBuffer);
17043
+ if (hasExtension(nvImage.hdr)) {
17044
+ nvImage.extensions = nvImage.hdr.extensions;
17045
+ }
17011
17046
  if (nvImage.hdr === null) {
17012
17047
  throw new Error(`Failed to read NIfTI header: ${nvImage.name}`);
17013
17048
  }
@@ -17378,6 +17413,7 @@ var NVImage = class _NVImage {
17378
17413
  __publicField(this, "extentsMaxOrtho");
17379
17414
  __publicField(this, "mm2ortho");
17380
17415
  __publicField(this, "hdr", null);
17416
+ __publicField(this, "extensions");
17381
17417
  __publicField(this, "imageType");
17382
17418
  __publicField(this, "img");
17383
17419
  __publicField(this, "imaginary");
@@ -20785,6 +20821,7 @@ var DRAG_MODE = /* @__PURE__ */ ((DRAG_MODE2) => {
20785
20821
  DRAG_MODE2[DRAG_MODE2["slicer3D"] = 4] = "slicer3D";
20786
20822
  DRAG_MODE2[DRAG_MODE2["callbackOnly"] = 5] = "callbackOnly";
20787
20823
  DRAG_MODE2[DRAG_MODE2["roiSelection"] = 6] = "roiSelection";
20824
+ DRAG_MODE2[DRAG_MODE2["angle"] = 7] = "angle";
20788
20825
  return DRAG_MODE2;
20789
20826
  })(DRAG_MODE || {});
20790
20827
  var DRAG_MODE_SECONDARY = /* @__PURE__ */ ((DRAG_MODE_SECONDARY2) => {
@@ -20795,6 +20832,7 @@ var DRAG_MODE_SECONDARY = /* @__PURE__ */ ((DRAG_MODE_SECONDARY2) => {
20795
20832
  DRAG_MODE_SECONDARY2[DRAG_MODE_SECONDARY2["slicer3D"] = 4] = "slicer3D";
20796
20833
  DRAG_MODE_SECONDARY2[DRAG_MODE_SECONDARY2["callbackOnly"] = 5] = "callbackOnly";
20797
20834
  DRAG_MODE_SECONDARY2[DRAG_MODE_SECONDARY2["roiSelection"] = 6] = "roiSelection";
20835
+ DRAG_MODE_SECONDARY2[DRAG_MODE_SECONDARY2["angle"] = 7] = "angle";
20798
20836
  return DRAG_MODE_SECONDARY2;
20799
20837
  })(DRAG_MODE_SECONDARY || {});
20800
20838
  var DRAG_MODE_PRIMARY = /* @__PURE__ */ ((DRAG_MODE_PRIMARY2) => {
@@ -25800,7 +25838,9 @@ var Niivue = class {
25800
25838
  lastTwoTouchDistance: 0,
25801
25839
  multiTouchGesture: false,
25802
25840
  windowX: 0,
25803
- windowY: 0
25841
+ windowY: 0,
25842
+ angleFirstLine: [0, 0, 0, 0],
25843
+ angleState: "none"
25804
25844
  });
25805
25845
  __publicField(this, "back", null);
25806
25846
  // base layer; defines image space to work in. Defined as this.volumes[0] in Niivue.loadVolumes
@@ -25900,6 +25940,7 @@ var Niivue = class {
25900
25940
  __publicField(this, "dragModes", {
25901
25941
  contrast: 1 /* contrast */,
25902
25942
  measurement: 2 /* measurement */,
25943
+ angle: 7 /* angle */,
25903
25944
  none: 0 /* none */,
25904
25945
  pan: 3 /* pan */,
25905
25946
  slicer3D: 4 /* slicer3D */,
@@ -26624,8 +26665,10 @@ var Niivue = class {
26624
26665
  this.drawPenLocation = [NaN, NaN, NaN];
26625
26666
  this.drawPenAxCorSag = -1;
26626
26667
  this.uiData.mousedown = true;
26627
- this.setDragStart(0, 0);
26628
- this.setDragEnd(0, 0);
26668
+ if (!(this.opts.dragMode === 7 /* angle */ && this.uiData.angleState === "drawing_second_line")) {
26669
+ this.setDragStart(0, 0);
26670
+ this.setDragEnd(0, 0);
26671
+ }
26629
26672
  log.debug("mouse down");
26630
26673
  log.debug(e);
26631
26674
  const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
@@ -26717,6 +26760,18 @@ var Niivue = class {
26717
26760
  if (this.opts.dragMode === 0 /* none */) {
26718
26761
  return;
26719
26762
  }
26763
+ if (this.opts.dragMode === 7 /* angle */) {
26764
+ if (this.uiData.angleState === "none") {
26765
+ this.uiData.angleState = "drawing_first_line";
26766
+ } else if (this.uiData.angleState === "drawing_second_line") {
26767
+ this.uiData.angleState = "complete";
26768
+ this.drawScene();
26769
+ return;
26770
+ } else if (this.uiData.angleState === "complete") {
26771
+ this.resetAngleMeasurement();
26772
+ this.uiData.angleState = "drawing_first_line";
26773
+ }
26774
+ }
26720
26775
  this.setDragStart(pos.x, pos.y);
26721
26776
  if (!this.uiData.isDragging) {
26722
26777
  this.uiData.pan2DxyzmmAtMouseDown = vec410.clone(this.scene.pan2Dxyzmm);
@@ -26866,6 +26921,24 @@ var Niivue = class {
26866
26921
  }
26867
26922
  if (this.uiData.isDragging) {
26868
26923
  this.uiData.isDragging = false;
26924
+ if (this.opts.dragMode === 7 /* angle */) {
26925
+ if (this.uiData.angleState === "drawing_first_line") {
26926
+ this.uiData.angleFirstLine = [
26927
+ this.uiData.dragStart[0],
26928
+ this.uiData.dragStart[1],
26929
+ this.uiData.dragEnd[0],
26930
+ this.uiData.dragEnd[1]
26931
+ ];
26932
+ this.uiData.angleState = "drawing_second_line";
26933
+ this.uiData.isDragging = true;
26934
+ this.drawScene();
26935
+ return;
26936
+ } else if (this.uiData.angleState === "drawing_second_line") {
26937
+ this.uiData.angleState = "complete";
26938
+ this.drawScene();
26939
+ return;
26940
+ }
26941
+ }
26869
26942
  if (this.opts.dragMode === 5 /* callbackOnly */) {
26870
26943
  this.drawScene();
26871
26944
  }
@@ -27062,6 +27135,13 @@ var Niivue = class {
27062
27135
  this.drawScene();
27063
27136
  this.uiData.prevX = this.uiData.currX;
27064
27137
  this.uiData.prevY = this.uiData.currY;
27138
+ } else if (this.opts.dragMode === 7 /* angle */ && this.uiData.angleState === "drawing_second_line") {
27139
+ const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
27140
+ if (!pos) {
27141
+ return;
27142
+ }
27143
+ this.setDragEnd(pos.x, pos.y);
27144
+ this.drawScene();
27065
27145
  } else if (!this.uiData.mousedown && this.opts.clickToSegment) {
27066
27146
  const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas);
27067
27147
  if (!pos) {
@@ -34072,7 +34152,7 @@ var Niivue = class {
34072
34152
  * Draw a measurement line with end caps and length text on a 2D tile.
34073
34153
  * @internal
34074
34154
  */
34075
- drawMeasurementTool(startXYendXY) {
34155
+ drawMeasurementTool(startXYendXY, isDrawText = true) {
34076
34156
  function extendTo(x0, y0, x1, y1, distance) {
34077
34157
  const x = x0 - x1;
34078
34158
  const y = y0 - y1;
@@ -34151,16 +34231,153 @@ var Niivue = class {
34151
34231
  textCoords = startXYendXY;
34152
34232
  break;
34153
34233
  }
34154
- this.drawTextBetween(
34155
- textCoords,
34156
- stringMM,
34157
- this.opts.measureTextHeight / 0.06,
34158
- // <- TODO measureFontPx
34159
- this.opts.measureTextColor
34160
- );
34234
+ if (isDrawText) {
34235
+ this.drawTextBetween(
34236
+ textCoords,
34237
+ stringMM,
34238
+ this.opts.measureTextHeight / 0.06,
34239
+ // <- TODO measureFontPx
34240
+ this.opts.measureTextColor
34241
+ );
34242
+ }
34161
34243
  }
34162
34244
  gl.bindVertexArray(this.unusedVAO);
34163
34245
  }
34246
+ /**
34247
+ * Draw angle measurement tool with two lines and angle display.
34248
+ * @internal
34249
+ */
34250
+ drawAngleMeasurementTool() {
34251
+ if (this.uiData.angleState === "drawing_first_line") {
34252
+ this.drawMeasurementTool(
34253
+ [this.uiData.dragStart[0], this.uiData.dragStart[1], this.uiData.dragEnd[0], this.uiData.dragEnd[1]],
34254
+ false
34255
+ );
34256
+ } else if (this.uiData.angleState === "drawing_second_line") {
34257
+ this.drawMeasurementTool(this.uiData.angleFirstLine, false);
34258
+ this.drawMeasurementTool(
34259
+ [
34260
+ this.uiData.angleFirstLine[2],
34261
+ // start from end of first line
34262
+ this.uiData.angleFirstLine[3],
34263
+ this.uiData.dragEnd[0],
34264
+ // to current mouse position
34265
+ this.uiData.dragEnd[1]
34266
+ ],
34267
+ false
34268
+ );
34269
+ this.drawAngleText();
34270
+ } else if (this.uiData.angleState === "complete") {
34271
+ this.drawMeasurementTool(this.uiData.angleFirstLine, false);
34272
+ const secondLine = [
34273
+ this.uiData.angleFirstLine[2],
34274
+ // start from end of first line
34275
+ this.uiData.angleFirstLine[3],
34276
+ this.uiData.dragEnd[0],
34277
+ // to final position
34278
+ this.uiData.dragEnd[1]
34279
+ ];
34280
+ this.drawMeasurementTool(secondLine, false);
34281
+ this.drawAngleText();
34282
+ }
34283
+ }
34284
+ /**
34285
+ * Calculate and draw angle text at the intersection of two lines.
34286
+ * @internal
34287
+ */
34288
+ drawAngleText() {
34289
+ const line1 = this.uiData.angleFirstLine;
34290
+ const line2 = [
34291
+ this.uiData.angleFirstLine[2],
34292
+ // start from end of first line
34293
+ this.uiData.angleFirstLine[3],
34294
+ this.uiData.dragEnd[0],
34295
+ // to current mouse position
34296
+ this.uiData.dragEnd[1]
34297
+ ];
34298
+ const angle = this.calculateAngleBetweenLines(line1, line2);
34299
+ const intersectionX = this.uiData.angleFirstLine[2];
34300
+ const intersectionY = this.uiData.angleFirstLine[3];
34301
+ const angleText = `${angle.toFixed(1)}\xB0`;
34302
+ this.drawTextBetween(
34303
+ [intersectionX, intersectionY, intersectionX + 1, intersectionY + 1],
34304
+ angleText,
34305
+ this.opts.measureTextHeight / 0.06,
34306
+ this.opts.measureTextColor
34307
+ );
34308
+ }
34309
+ /**
34310
+ * Calculate angle between two lines in degrees.
34311
+ * @internal
34312
+ */
34313
+ calculateAngleBetweenLines(line1, line2) {
34314
+ const intersectionX = line1[2];
34315
+ const intersectionY = line1[3];
34316
+ const v1x = line1[0] - intersectionX;
34317
+ const v1y = line1[1] - intersectionY;
34318
+ const v2x = line2[2] - intersectionX;
34319
+ const v2y = line2[3] - intersectionY;
34320
+ const dot = v1x * v2x + v1y * v2y;
34321
+ const mag1 = Math.sqrt(v1x * v1x + v1y * v1y);
34322
+ const mag2 = Math.sqrt(v2x * v2x + v2y * v2y);
34323
+ if (mag1 === 0 || mag2 === 0) {
34324
+ return 0;
34325
+ }
34326
+ const cosAngle = Math.max(-1, Math.min(1, dot / (mag1 * mag2)));
34327
+ const angleRad = Math.acos(cosAngle);
34328
+ const angleDeg = angleRad * (180 / Math.PI);
34329
+ return angleDeg;
34330
+ }
34331
+ /**
34332
+ * Reset the angle measurement state.
34333
+ * @internal
34334
+ */
34335
+ resetAngleMeasurement() {
34336
+ this.uiData.angleState = "none";
34337
+ this.uiData.angleFirstLine = [0, 0, 0, 0];
34338
+ }
34339
+ /**
34340
+ * Set the drag mode for mouse interactions.
34341
+ * @param mode - The drag mode to set ('none', 'contrast', 'measurement', 'angle', 'pan', 'slicer3D', 'callbackOnly', 'roiSelection')
34342
+ */
34343
+ setDragMode(mode) {
34344
+ if (typeof mode === "string") {
34345
+ switch (mode) {
34346
+ case "none":
34347
+ this.opts.dragMode = 0 /* none */;
34348
+ break;
34349
+ case "contrast":
34350
+ this.opts.dragMode = 1 /* contrast */;
34351
+ break;
34352
+ case "measurement":
34353
+ this.opts.dragMode = 2 /* measurement */;
34354
+ break;
34355
+ case "angle":
34356
+ this.opts.dragMode = 7 /* angle */;
34357
+ break;
34358
+ case "pan":
34359
+ this.opts.dragMode = 3 /* pan */;
34360
+ break;
34361
+ case "slicer3D":
34362
+ this.opts.dragMode = 4 /* slicer3D */;
34363
+ break;
34364
+ case "callbackOnly":
34365
+ this.opts.dragMode = 5 /* callbackOnly */;
34366
+ break;
34367
+ case "roiSelection":
34368
+ this.opts.dragMode = 6 /* roiSelection */;
34369
+ break;
34370
+ default:
34371
+ console.warn(`Unknown drag mode: ${mode}`);
34372
+ return;
34373
+ }
34374
+ } else {
34375
+ this.opts.dragMode = mode;
34376
+ }
34377
+ if (this.opts.dragMode !== 7 /* angle */) {
34378
+ this.resetAngleMeasurement();
34379
+ }
34380
+ }
34164
34381
  /**
34165
34382
  * Draw a rectangle or outline at given position with specified color or default crosshair color.
34166
34383
  * @internal
@@ -37421,6 +37638,10 @@ var Niivue = class {
37421
37638
  ]);
37422
37639
  return;
37423
37640
  }
37641
+ if (this.opts.dragMode === 7 /* angle */) {
37642
+ this.drawAngleMeasurementTool();
37643
+ return;
37644
+ }
37424
37645
  const width = Math.abs(this.uiData.dragStart[0] - this.uiData.dragEnd[0]);
37425
37646
  const height = Math.abs(this.uiData.dragStart[1] - this.uiData.dragEnd[1]);
37426
37647
  this.drawSelectionBox([