@niivue/niivue 0.24.3 → 0.26.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/niivue.js CHANGED
@@ -1,10 +1,16 @@
1
1
  import { Shader } from "./shader.js";
2
2
  import * as mat from "gl-matrix";
3
- import { vertSliceShader, fragSliceShader } from "./shader-srcs.js";
3
+ import {
4
+ vertSliceMMShader,
5
+ fragSliceMMShader,
6
+ vertSliceShader,
7
+ fragSliceShader,
8
+ } from "./shader-srcs.js";
4
9
  import {
5
10
  vertGraphShader,
11
+ vertRectShader,
6
12
  vertLineShader,
7
- fragLineShader,
13
+ fragRectShader,
8
14
  } from "./shader-srcs.js";
9
15
  import { vertRenderShader, fragRenderShader } from "./shader-srcs.js";
10
16
  import { vertColorbarShader, fragColorbarShader } from "./shader-srcs.js";
@@ -32,6 +38,7 @@ import {
32
38
  fragMeshOutlineShader,
33
39
  fragMeshHemiShader,
34
40
  fragMeshMatteShader,
41
+ fragMeshDepthShader,
35
42
  fragMeshShaderSHBlue,
36
43
  vertFiberShader,
37
44
  fragFiberShader,
@@ -94,11 +101,13 @@ export function Niivue(options = {}) {
94
101
  textHeight: 0.06, // 0 for no text, fraction of canvas min(height,width)
95
102
  colorbarHeight: 0.05, // 0 for no colorbars, fraction of Nifti j dimension
96
103
  crosshairWidth: 1, // 0 for no crosshairs
104
+ rulerWidth: 4,
97
105
  show3Dcrosshair: false,
98
106
  backColor: [0, 0, 0, 1],
99
107
  crosshairColor: [1, 0, 0, 1],
100
108
  selectionBoxColor: [1, 1, 1, 0.5],
101
109
  clipPlaneColor: [0.7, 0, 0.7, 0.5],
110
+ rulerColor: [1, 0, 0, 0.8],
102
111
  colorBarMargin: 0.05, // x axis margin arount color bar, clip space coordinates
103
112
  trustCalMinMax: true, // trustCalMinMax: if true do not calculate cal_min or cal_max if set in image header. If false, always calculate display intensity range.
104
113
  clipPlaneHotKey: "KeyC", // keyboard short cut to activate the clip plane
@@ -106,7 +115,14 @@ export function Niivue(options = {}) {
106
115
  keyDebounceTime: 50, // default debounce time used in keyup listeners
107
116
  isNearestInterpolation: false,
108
117
  isAtlasOutline: false,
118
+ isRuler: false,
119
+ isColorbar: false,
109
120
  isRadiologicalConvention: false,
121
+ meshThicknessOn2D: Infinity,
122
+ isDragShowsMeasurementTool: false,
123
+ isCornerOrientationText: false,
124
+ sagittalNoseLeft: false, //sagittal slices can have Y+ going left or right
125
+ isSliceMM: false,
110
126
  logging: false,
111
127
  loadingText: "waiting for images...",
112
128
  dragAndDropEnabled: true,
@@ -114,6 +130,9 @@ export function Niivue(options = {}) {
114
130
  penValue: 1, // sets drawing color. see "drawPt"
115
131
  isFilledPen: false,
116
132
  thumbnail: "",
133
+ onLocationChange: () => {},
134
+ onIntensityChange: () => {},
135
+ onImageLoaded: () => {},
117
136
  };
118
137
 
119
138
  this.canvas = null; // the canvas element on the page
@@ -126,6 +145,7 @@ export function Niivue(options = {}) {
126
145
  this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //analogy: cylinder position of a revolver
127
146
  this.drawUndoBitmaps = [];
128
147
  this.drawOpacity = 0.8;
148
+ this.colorbarHeight = 0; //height in pixels, set when colorbar is drawn
129
149
  this.drawPenLocation = [NaN, NaN, NaN];
130
150
  this.drawPenAxCorSag = -1; //do not allow pen to drag between Sagittal/Coronal/Axial
131
151
  this.drawFillOverwrites = true;
@@ -133,7 +153,8 @@ export function Niivue(options = {}) {
133
153
  this.overlayTexture = null;
134
154
  this.overlayTextureID = [];
135
155
  this.sliceShader = null;
136
- this.lineShader = null;
156
+ this.sliceMMShader = null;
157
+ this.rectShader = null;
137
158
  this.graphShader = null;
138
159
  this.renderShader = null;
139
160
  this.pickingShader = null;
@@ -167,6 +188,8 @@ export function Niivue(options = {}) {
167
188
  this.sliceTypeSagittal = 2;
168
189
  this.sliceTypeMultiplanar = 3;
169
190
  this.sliceTypeRender = 4;
191
+ //this.sliceTypeMosaic = 5; //kava
192
+ this.sliceMosaicString = "";
170
193
  this.sliceType = this.sliceTypeMultiplanar; // sets current view in webgl canvas
171
194
  this.scene = {};
172
195
  this.syncOpts = {};
@@ -183,6 +206,7 @@ export function Niivue(options = {}) {
183
206
  this.scene.mouseButtonLeftDown = false;
184
207
  this.scene.mouseButtonRightDown = false;
185
208
  this.scene.mouseDepthPicker = false;
209
+ this.scene.pan2Dxyzmm = [0, 0, 0, 1];
186
210
  this.scene.scale2D = 1.0;
187
211
  this.scene.prevX = 0;
188
212
  this.scene.prevY = 0;
@@ -199,14 +223,7 @@ export function Niivue(options = {}) {
199
223
  this.volScale = [];
200
224
  this.vox = [];
201
225
  this.mousePos = [0, 0];
202
- this.numScreenSlices = 0; // e.g. for multiplanar view, 3 simultaneous slices: axial, coronal, sagittal
203
- this.screenSlices = [
204
- //location and type of each 2D slice on screen, allows clicking to detect position
205
- { leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
206
- { leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
207
- { leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
208
- { leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
209
- ];
226
+ this.screenSlices = []; // empty array
210
227
  this.isDragging = false;
211
228
  this.dragStart = [0.0, 0.0];
212
229
  this.dragEnd = [0.0, 0.0];
@@ -216,10 +233,7 @@ export function Niivue(options = {}) {
216
233
  this.volumeObject3D = null;
217
234
  this.pivot3D = [0, 0, 0]; //center for rendering rotation
218
235
  this.furthestFromPivot = 10.0; //most distant point from pivot
219
- this.intensityRange$ = new Subject(); // an array
220
- this.scene.location$ = new Subject(); // object with properties: {mm: [N N N], vox: [N N N], frac: [N N N]}
221
236
  this.scene.loading$ = new Subject(); // whether or not the scene is loading
222
- this.imageLoaded$ = new Subject();
223
237
  this.currentClipPlaneIndex = 0;
224
238
  this.lastCalled = new Date().getTime();
225
239
  this.multiTouchGesture = false;
@@ -260,6 +274,10 @@ export function Niivue(options = {}) {
260
274
  Name: "Toon",
261
275
  Frag: fragMeshToonShader,
262
276
  },
277
+ {
278
+ Name: "Depth",
279
+ Frag: fragMeshDepthShader,
280
+ },
263
281
  ];
264
282
 
265
283
  // multiuser
@@ -293,16 +311,37 @@ export function Niivue(options = {}) {
293
311
 
294
312
  // maping of keys (event strings) to rxjs subjects
295
313
  this.eventsToSubjects = {
296
- location: this.scene.location$,
297
314
  loading: this.scene.loading$,
298
- imageLoaded: this.imageLoaded$,
299
- intensityRange: this.intensityRange$,
300
315
  };
301
316
 
302
317
  // rxjs subscriptions. Keeping a reference array like this allows us to unsubscribe later
303
318
  this.subscriptions = [];
304
319
  }
305
320
 
321
+ Niivue.prototype.saveScene = function (filename = "") {
322
+ function saveBlob(blob, name) {
323
+ const a = document.createElement("a");
324
+ document.body.appendChild(a);
325
+ a.style.display = "none";
326
+ const url = window.URL.createObjectURL(blob);
327
+ a.href = url;
328
+ a.download = name;
329
+ a.click();
330
+ a.remove();
331
+ }
332
+
333
+ let canvas = this.canvas;
334
+ this.drawScene();
335
+ canvas.toBlob((blob) => {
336
+ if (filename === "") {
337
+ filename = `niivue-screenshot-${new Date().toString()}.png`;
338
+ filename = filename.replace(/\s/g, "_");
339
+ }
340
+ console.log(filename);
341
+ saveBlob(blob, filename);
342
+ });
343
+ };
344
+
306
345
  /**
307
346
  * attach the Niivue instance to the webgl2 canvas by element id
308
347
  * @param {string} id the id of an html canvas element
@@ -333,9 +372,6 @@ Niivue.prototype.attachTo = async function (id) {
333
372
  * // data has the shape {mm: [N, N, N], vox: [N, N, N], frac: [N, N, N], values: this.volumes.map(v => {return val})}
334
373
  * //...
335
374
  * }
336
- * niivue.on('location', doSomethingWithLocationData)
337
- * niivue.on('intensityRange', callback)
338
- * niivue.on('imageLoaded', callback)
339
375
  */
340
376
  Niivue.prototype.on = function (event, callback) {
341
377
  let knownEvents = Object.keys(this.eventsToSubjects);
@@ -714,7 +750,10 @@ function intensityRaw2Scaled(hdr, raw) {
714
750
  // not included in public docs
715
751
  // note: no test yet
716
752
  Niivue.prototype.calculateNewRange = function (volIdx = 0) {
717
- if (this.sliceType === this.sliceTypeRender) {
753
+ if (
754
+ this.sliceType === this.sliceTypeRender &&
755
+ this.sliceMosaicString.length < 1
756
+ ) {
718
757
  return;
719
758
  }
720
759
  if (
@@ -724,9 +763,10 @@ Niivue.prototype.calculateNewRange = function (volIdx = 0) {
724
763
  return;
725
764
  // calculate our box
726
765
  let frac = this.canvasPos2frac([this.dragStart[0], this.dragStart[1]]);
766
+ if (frac[0] < 0) return;
727
767
  let startVox = this.frac2vox(frac, volIdx);
728
-
729
768
  frac = this.canvasPos2frac([this.dragEnd[0], this.dragEnd[1]]);
769
+ if (frac[0] < 0) return;
730
770
  let endVox = this.frac2vox(frac, volIdx);
731
771
 
732
772
  let hi = -Number.MAX_VALUE,
@@ -773,7 +813,7 @@ Niivue.prototype.calculateNewRange = function (volIdx = 0) {
773
813
  var mxScale = intensityRaw2Scaled(hdr, hi);
774
814
  this.volumes[volIdx].cal_min = mnScale;
775
815
  this.volumes[volIdx].cal_max = mxScale;
776
- this.intensityRange$.next(this.volumes[volIdx]); //reference to volume to access cal_min and cal_max
816
+ this.opts.onIntensityChange(this.volumes[volIdx]);
777
817
  };
778
818
 
779
819
  // not included in public docs
@@ -787,8 +827,10 @@ Niivue.prototype.mouseUpListener = function () {
787
827
  else if (this.drawPenAxCorSag >= 0) this.drawAddUndoBitmap();
788
828
  this.drawPenLocation = [NaN, NaN, NaN];
789
829
  this.drawPenAxCorSag = -1;
830
+
790
831
  if (this.isDragging) {
791
832
  this.isDragging = false;
833
+ if (this.opts.isDragShowsMeasurementTool) return;
792
834
  this.calculateNewRange();
793
835
  this.refreshLayers(this.volumes[0], 0, this.volumes.length);
794
836
  }
@@ -861,19 +903,28 @@ Niivue.prototype.mouseMoveListener = function (e) {
861
903
  // note: should update this to accept a volume index to reset a selected volume rather than only the background (TODO)
862
904
  // reset brightness and contrast to global min and max
863
905
  // note: no test yet
864
- Niivue.prototype.resetBriCon = function () {
906
+ Niivue.prototype.resetBriCon = function (msg = null) {
865
907
  //this.volumes[0].cal_min = this.volumes[0].global_min;
866
908
  //this.volumes[0].cal_max = this.volumes[0].global_max;
867
909
 
868
910
  // don't reset bri/con if the user is in 3D mode and double clicks
911
+ let isRender = false;
869
912
  if (this.sliceType === this.sliceTypeRender) {
913
+ isRender = true;
914
+ }
915
+ if (msg !== null) {
916
+ // test if render is one of the tiles
917
+ if (this.inRenderTile(msg.offsetX, msg.offsetY) >= 0) isRender = true;
918
+ }
919
+ if (isRender) {
870
920
  this.scene.mouseDepthPicker = true;
871
921
  this.drawScene();
922
+ this.drawScene();
872
923
  return;
873
924
  }
874
925
  this.volumes[0].cal_min = this.volumes[0].robust_min;
875
926
  this.volumes[0].cal_max = this.volumes[0].robust_max;
876
- this.intensityRange$.next(this.volumes[0]);
927
+ this.opts.onIntensityChange(this.volumes[0]);
877
928
  this.refreshLayers(this.volumes[0], 0, this.volumes.length);
878
929
  this.drawScene();
879
930
  };
@@ -937,9 +988,9 @@ Niivue.prototype.handlePinchZoom = function (e) {
937
988
  // handler for keyboard shortcuts
938
989
  Niivue.prototype.keyUpListener = function (e) {
939
990
  if (e.code === this.opts.clipPlaneHotKey) {
940
- if (this.sliceType != this.sliceTypeRender) {
991
+ /*if (this.sliceType != this.sliceTypeRender) {
941
992
  return;
942
- }
993
+ }*/ //bravo
943
994
  let now = new Date().getTime();
944
995
  let elapsed = now - this.lastCalled;
945
996
  if (elapsed > this.opts.keyDebounceTime) {
@@ -1290,10 +1341,32 @@ Niivue.prototype.dropListener = async function (e) {
1290
1341
  this.drawScene(); //<- this seems to be required if you drag and drop a mesh, not a volume
1291
1342
  };
1292
1343
 
1344
+ Niivue.prototype.setCornerOrientationText = function (isCornerOrientationText) {
1345
+ this.opts.isCornerOrientationText = isCornerOrientationText;
1346
+ this.updateGLVolume();
1347
+ };
1348
+
1293
1349
  Niivue.prototype.setRadiologicalConvention = function (
1294
1350
  isRadiologicalConvention
1295
1351
  ) {
1296
1352
  this.opts.isRadiologicalConvention = isRadiologicalConvention;
1353
+ this.updateGLVolume();
1354
+ };
1355
+
1356
+ Niivue.prototype.setMeshThicknessOn2D = function (meshThicknessOn2D) {
1357
+ this.opts.meshThicknessOn2D = meshThicknessOn2D;
1358
+ this.updateGLVolume();
1359
+ };
1360
+
1361
+ Niivue.prototype.setSliceMosaicString = function (str) {
1362
+ this.sliceMosaicString = str;
1363
+ this.updateGLVolume();
1364
+ };
1365
+
1366
+ Niivue.prototype.setSliceMM = function (isSliceMM) {
1367
+ this.opts.isSliceMM = isSliceMM;
1368
+ console.log("SliceMM feature is experimental");
1369
+ this.updateGLVolume();
1297
1370
  };
1298
1371
 
1299
1372
  Niivue.prototype.getRadiologicalConvention = function () {
@@ -1311,7 +1384,7 @@ Niivue.prototype.addVolume = function (volume) {
1311
1384
  this.volumes.push(volume);
1312
1385
  let idx = this.volumes.length === 1 ? 0 : this.volumes.length - 1;
1313
1386
  this.setVolume(volume, idx);
1314
- this.imageLoaded$.next(volume); // pass reference to the loaded NVImage (the volume)
1387
+ this.opts.onImageLoaded(volume);
1315
1388
  };
1316
1389
 
1317
1390
  /**
@@ -1325,7 +1398,7 @@ Niivue.prototype.addMesh = function (mesh) {
1325
1398
  this.meshes.push(mesh);
1326
1399
  let idx = this.meshes.length === 1 ? 0 : this.meshes.length - 1;
1327
1400
  this.setMesh(mesh, idx);
1328
- this.imageLoaded$.next(mesh); // pass reference to the loaded NVImage (the volume)
1401
+ this.opts.onImageLoaded(mesh);
1329
1402
  };
1330
1403
 
1331
1404
  /**
@@ -1502,17 +1575,6 @@ Niivue.prototype.drawUndo = function () {
1502
1575
  this.refreshDrawing(true);
1503
1576
  };
1504
1577
 
1505
- /*Niivue.prototype.drawUndo = function () {
1506
- let hdr = this.back.hdr;
1507
- let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
1508
- if (this.drawUndoBitmap.length !== nv || this.drawBitmap.length !== nv) {
1509
- console.log("bitmap dims are wrong");
1510
- return;
1511
- }
1512
- this.drawBitmap = this.drawUndoBitmap.slice();
1513
- this.refreshDrawing(true);
1514
- };*/
1515
-
1516
1578
  Niivue.prototype.loadDrawing = async function (fnm) {
1517
1579
  if (this.drawBitmap) console.log("Overwriting open drawing!");
1518
1580
  this.drawClearAllUndoBitmaps();
@@ -1700,6 +1762,7 @@ Niivue.prototype.reverseFaces = function (mesh) {
1700
1762
  this.meshes[idx].reverseFaces(this.gl);
1701
1763
  this.updateGLVolume();
1702
1764
  };
1765
+
1703
1766
  Niivue.prototype.setMeshLayerProperty = function (mesh, layer, key, val) {
1704
1767
  let idx = this.getMeshIndexByID(mesh);
1705
1768
  if (idx < 0) {
@@ -1710,17 +1773,21 @@ Niivue.prototype.setMeshLayerProperty = function (mesh, layer, key, val) {
1710
1773
  this.updateGLVolume();
1711
1774
  };
1712
1775
 
1776
+ Niivue.prototype.setPan2Dxyzmm = function (xyzmmZoom) {
1777
+ this.scene.pan2Dxyzmm = xyzmmZoom;
1778
+ this.drawScene();
1779
+ };
1780
+
1713
1781
  Niivue.prototype.setScale2D = function (zoomFactor) {
1714
1782
  this.scene.scale2D = zoomFactor;
1715
- console.log("bingo");
1716
1783
  this.drawScene();
1717
- }; // mouseMove()
1784
+ }; // setScale2D()
1718
1785
 
1719
1786
  Niivue.prototype.setRenderAzimuthElevation = function (a, e) {
1720
1787
  this.scene.renderAzimuth = a;
1721
1788
  this.scene.renderElevation = e;
1722
1789
  this.drawScene();
1723
- }; // mouseMove()
1790
+ }; // setRenderAzimuthElevation()
1724
1791
 
1725
1792
  /**
1726
1793
  * get the index of an overlay by its unique id. unique ids are assigned to the NVImage.id property when a new NVImage is created.
@@ -1867,14 +1934,14 @@ Niivue.prototype.moveVolumeToTop = function (volume) {
1867
1934
  // update mouse position from new mouse down coordinates
1868
1935
  // note: no test yet
1869
1936
  Niivue.prototype.mouseDown = function mouseDown(x, y) {
1870
- if (this.sliceType != this.sliceTypeRender) return;
1937
+ if (this.inRenderTile(x, y) < 0) return;
1871
1938
  this.mousePos = [x, y];
1872
1939
  }; // mouseDown()
1873
1940
 
1874
1941
  // not included in public docs
1875
1942
  // note: no test yet
1876
1943
  Niivue.prototype.mouseMove = function mouseMove(x, y) {
1877
- if (this.sliceType != this.sliceTypeRender) return;
1944
+ if (this.inRenderTile(x, y) < 0) return;
1878
1945
  this.scene.renderAzimuth += x - this.mousePos[0];
1879
1946
  this.scene.renderElevation += y - this.mousePos[1];
1880
1947
  this.mousePos = [x, y];
@@ -1927,7 +1994,7 @@ Niivue.prototype.setClipPlane = function (depthAzimuthElevation) {
1927
1994
  );
1928
1995
  this.scene.clipPlane = [v[0], v[1], v[2], depthAzimuthElevation[0]];
1929
1996
  this.scene.clipPlaneDepthAziElev = depthAzimuthElevation;
1930
- if (this.sliceType != this.sliceTypeRender) return;
1997
+ //if (this.sliceType != this.sliceTypeRender) return;
1931
1998
  this.drawScene();
1932
1999
  }; // setClipPlane()
1933
2000
 
@@ -2129,6 +2196,7 @@ Niivue.prototype.loadVolumes = async function (volumeList) {
2129
2196
  url: volumeList[i].url,
2130
2197
  name: volumeList[i].name,
2131
2198
  colorMap: volumeList[i].colorMap,
2199
+ colorMapNegative: volumeList[i].colorMapNegative,
2132
2200
  opacity: volumeList[i].opacity,
2133
2201
  urlImgData: volumeList[i].urlImgData,
2134
2202
  cal_min: volumeList[i].cal_min,
@@ -2522,7 +2590,7 @@ Niivue.prototype.drawPt = function (x, y, z, penValue) {
2522
2590
  //https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
2523
2591
  // https://www.geeksforgeeks.org/bresenhams-algorithm-for-3-d-line-drawing/
2524
2592
  // ptA, ptB are start and end points of line (each XYZ)
2525
- Niivue.prototype.drawLine = function (ptA, ptB, penValue) {
2593
+ Niivue.prototype.drawPenLine = function (ptA, ptB, penValue) {
2526
2594
  let dx = Math.abs(ptA[0] - ptB[0]);
2527
2595
  let dy = Math.abs(ptA[1] - ptB[1]);
2528
2596
  let dz = Math.abs(ptA[2] - ptB[2]);
@@ -3271,13 +3339,28 @@ Niivue.prototype.init = async function () {
3271
3339
  this.sliceShader = new Shader(this.gl, vertSliceShader, fragSliceShader);
3272
3340
  this.sliceShader.use(this.gl);
3273
3341
  this.gl.uniform1i(this.sliceShader.uniforms["volume"], 0);
3274
- //this.gl.uniform1i(this.sliceShader.uniforms["colormap"], 1); //orient shader applies colormap
3275
3342
  this.gl.uniform1i(this.sliceShader.uniforms["overlay"], 2);
3276
3343
  this.gl.uniform1i(this.sliceShader.uniforms["drawing"], 7);
3277
3344
  this.gl.uniform1f(this.sliceShader.uniforms["drawOpacity"], this.drawOpacity);
3278
- // line shader (crosshair)
3279
- this.lineShader = new Shader(this.gl, vertLineShader, fragLineShader);
3280
- this.graphShader = new Shader(this.gl, vertGraphShader, fragLineShader);
3345
+ // slice mm shader
3346
+ this.sliceMMShader = new Shader(
3347
+ this.gl,
3348
+ vertSliceMMShader,
3349
+ fragSliceMMShader
3350
+ );
3351
+ this.sliceMMShader.use(this.gl);
3352
+ this.gl.uniform1i(this.sliceMMShader.uniforms["volume"], 0);
3353
+ this.gl.uniform1i(this.sliceMMShader.uniforms["overlay"], 2);
3354
+ this.gl.uniform1i(this.sliceMMShader.uniforms["drawing"], 7);
3355
+ this.gl.uniform1f(
3356
+ this.sliceMMShader.uniforms["drawOpacity"],
3357
+ this.drawOpacity
3358
+ );
3359
+ // rect shader (crosshair): horizontal and vertical lines only
3360
+ this.rectShader = new Shader(this.gl, vertRectShader, fragRectShader);
3361
+ // line shader: diagonal lines
3362
+ this.lineShader = new Shader(this.gl, vertLineShader, fragRectShader);
3363
+ this.graphShader = new Shader(this.gl, vertGraphShader, fragRectShader);
3281
3364
  // render shader (3D)
3282
3365
  this.renderShader = new Shader(this.gl, vertRenderShader, fragRenderShader);
3283
3366
  this.renderShader.use(this.gl);
@@ -3506,7 +3589,7 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
3506
3589
  if (layer === 0) {
3507
3590
  this.volumeObject3D = overlayItem.toNiivueObject3D(this.VOLUME_ID, this.gl);
3508
3591
  mat.mat4.invert(mtx, mtx);
3509
- log.debug(`mtx layer ${layer}`, mtx);
3592
+ //log.debug(`mtx layer ${layer}`, mtx);
3510
3593
  this.back.matRAS = overlayItem.matRAS;
3511
3594
  this.back.dims = overlayItem.dimsRAS;
3512
3595
  this.back.pixDims = overlayItem.pixDimsRAS;
@@ -3819,10 +3902,81 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
3819
3902
  this.gl.uniform1i(orientShader.uniforms["blend3D"], 5);
3820
3903
  this.gl.uniform1i(orientShader.uniforms["colormap"], 1);
3821
3904
  this.gl.uniform1f(orientShader.uniforms["layer"], layer);
3822
- this.gl.uniform1f(orientShader.uniforms["numLayers"], numLayers);
3905
+ //this.gl.uniform1f(orientShader.uniforms["numLayers"], numLayers);
3823
3906
  this.gl.uniform1f(orientShader.uniforms["scl_inter"], hdr.scl_inter);
3824
3907
  this.gl.uniform1f(orientShader.uniforms["scl_slope"], hdr.scl_slope);
3825
3908
  this.gl.uniform1f(orientShader.uniforms["opacity"], opacity);
3909
+
3910
+ this.gl.uniform1i(orientShader.uniforms["modulationVol"], 7);
3911
+ let modulateTexture = null;
3912
+ if (
3913
+ overlayItem.modulationImage &&
3914
+ overlayItem.modulationImage >= 0 &&
3915
+ overlayItem.modulationImage < this.volumes.length
3916
+ ) {
3917
+ console.log(this.volumes);
3918
+ let mhdr = this.volumes[overlayItem.modulationImage].hdr;
3919
+ if (
3920
+ mhdr.dims[1] === hdr.dims[1] &&
3921
+ mhdr.dims[2] === hdr.dims[2] &&
3922
+ mhdr.dims[3] === hdr.dims[3]
3923
+ ) {
3924
+ this.gl.uniform1i(orientShader.uniforms["modulation"], 1);
3925
+
3926
+ //r8Tex(texID, activeID, dims, isInit = false)
3927
+ modulateTexture = this.r8Tex(
3928
+ modulateTexture,
3929
+ this.gl.TEXTURE7,
3930
+ hdr.dims,
3931
+ true
3932
+ );
3933
+ this.gl.activeTexture(this.gl.TEXTURE7);
3934
+ this.gl.bindTexture(this.gl.TEXTURE_3D, modulateTexture);
3935
+ let vx = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
3936
+ let modulateVolume = new Uint8Array(vx);
3937
+ let mn = mhdr.cal_min;
3938
+ let scale = 255.0 / (mhdr.cal_max - mhdr.cal_min);
3939
+ let imgRaw = this.volumes[overlayItem.modulationImage].img.buffer;
3940
+ let img = new Uint8Array(imgRaw);
3941
+ switch (mhdr.datatypeCode) {
3942
+ case overlayItem.DT_SIGNED_SHORT:
3943
+ img = new Int16Array(imgRaw);
3944
+ break;
3945
+ case overlayItem.DT_FLOAT:
3946
+ img = new Float32Array(imgRaw);
3947
+ break;
3948
+ case overlayItem.DT_DOUBLE:
3949
+ img = new Float64Array(imgRaw);
3950
+ break;
3951
+ case overlayItem.DT_RGB:
3952
+ img = new Uint8Array(imgRaw);
3953
+ break;
3954
+ case overlayItem.DT_UINT16:
3955
+ img = new Uint16Array(imgRaw);
3956
+ break;
3957
+ }
3958
+ console.log(this.volumes[overlayItem.modulationImage]);
3959
+ for (let i = 0; i < vx; i++) {
3960
+ let v = img[i] * mhdr.scl_slope + mhdr.scl_inter;
3961
+ v = (v - mn) * scale;
3962
+ v = Math.min(Math.max(v, 0.0), 255.0);
3963
+ modulateVolume[i] = v;
3964
+ }
3965
+ this.gl.texSubImage3D(
3966
+ this.gl.TEXTURE_3D,
3967
+ 0,
3968
+ 0,
3969
+ 0,
3970
+ 0,
3971
+ hdr.dims[1],
3972
+ hdr.dims[2],
3973
+ hdr.dims[3],
3974
+ this.gl.RED,
3975
+ this.gl.UNSIGNED_BYTE,
3976
+ modulateVolume
3977
+ );
3978
+ } else console.log("Modulation image dimensions do not match target");
3979
+ } else this.gl.uniform1i(orientShader.uniforms["modulation"], 0);
3826
3980
  this.gl.uniformMatrix4fv(orientShader.uniforms["mtx"], false, mtx);
3827
3981
  if (hdr.intent_code === 1002) {
3828
3982
  let x = 1.0 / this.back.dims[1];
@@ -3850,6 +4004,7 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
3850
4004
  }
3851
4005
  this.gl.bindVertexArray(this.unusedVAO);
3852
4006
  this.gl.deleteTexture(tempTex3D);
4007
+ this.gl.deleteTexture(modulateTexture);
3853
4008
  this.gl.deleteTexture(blendTexture);
3854
4009
  this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
3855
4010
  this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
@@ -3882,6 +4037,16 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
3882
4037
  this.overlays.length
3883
4038
  );
3884
4039
  this.gl.uniform3fv(this.pickingShader.uniforms["texVox"], vox);
4040
+ this.sliceMMShader.use(this.gl);
4041
+ this.gl.uniform1f(
4042
+ this.sliceMMShader.uniforms["overlays"],
4043
+ this.overlays.length
4044
+ );
4045
+ this.gl.uniform1f(
4046
+ this.sliceMMShader.uniforms["drawOpacity"],
4047
+ this.drawOpacity
4048
+ );
4049
+ this.gl.uniform1i(this.sliceMMShader.uniforms["drawing"], 7);
3885
4050
  // set overlays for slice shader
3886
4051
  this.sliceShader.use(this.gl);
3887
4052
  this.gl.uniform1f(
@@ -3890,6 +4055,8 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
3890
4055
  );
3891
4056
  this.gl.uniform1f(this.sliceShader.uniforms["drawOpacity"], this.drawOpacity);
3892
4057
  this.gl.uniform1i(this.sliceShader.uniforms["drawing"], 7);
4058
+ this.gl.activeTexture(this.gl.TEXTURE7);
4059
+ this.gl.bindTexture(this.gl.TEXTURE_3D, this.drawTexture);
3893
4060
  this.updateInterpolation(layer);
3894
4061
  //this.createEmptyDrawing(); //DO NOT DO THIS ON EVERY CALL TO REFRESH LAYERS!!!!
3895
4062
  //this.createRandomDrawing(); //DO NOT DO THIS ON EVERY CALL TO REFRESH LAYERS!!!!
@@ -3921,6 +4088,24 @@ Niivue.prototype.setColorMap = function (id, colorMap) {
3921
4088
  this.updateGLVolume();
3922
4089
  };
3923
4090
 
4091
+ Niivue.prototype.setColorMapNegative = function (id, colorMapNegative) {
4092
+ let idx = this.getVolumeIndexByID(id);
4093
+ this.volumes[idx].colorMapNegative = colorMapNegative;
4094
+ this.updateGLVolume();
4095
+ };
4096
+
4097
+ Niivue.prototype.setModulationImage = function (idTarget, idModulation) {
4098
+ //to set:
4099
+ // nv1.setModulationImage(nv1.volumes[0].id, nv1.volumes[1].id);
4100
+ //to clear:
4101
+ // nv1.setModulationImage(nv1.volumes[0].id, null);
4102
+ let idxTarget = this.getVolumeIndexByID(idTarget);
4103
+ let idxModulation = null;
4104
+ //if (idModulation)
4105
+ idxModulation = this.getVolumeIndexByID(idModulation);
4106
+ this.volumes[idxTarget].modulationImage = idxModulation;
4107
+ this.updateGLVolume();
4108
+ };
3924
4109
  Niivue.prototype.setGamma = function (gamma = 1.0) {
3925
4110
  cmapper.gamma = gamma;
3926
4111
  this.updateGLVolume();
@@ -3964,7 +4149,7 @@ Niivue.prototype.refreshColormaps = function () {
3964
4149
  this.colormapTexture = this.gl.createTexture();
3965
4150
  this.gl.activeTexture(this.gl.TEXTURE1);
3966
4151
  this.gl.bindTexture(this.gl.TEXTURE_2D, this.colormapTexture);
3967
- this.gl.texStorage2D(this.gl.TEXTURE_2D, 1, this.gl.RGBA8, 256, nLayer);
4152
+ this.gl.texStorage2D(this.gl.TEXTURE_2D, 1, this.gl.RGBA8, 256, nLayer * 2);
3968
4153
  this.gl.texParameteri(
3969
4154
  this.gl.TEXTURE_2D,
3970
4155
  this.gl.TEXTURE_MIN_FILTER,
@@ -3989,22 +4174,38 @@ Niivue.prototype.refreshColormaps = function () {
3989
4174
  );
3990
4175
  this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT, 1);
3991
4176
  let luts = this.colormap(this.volumes[0].colorMap);
3992
- if (nLayer > 1) {
3993
- for (let i = 1; i < nLayer; i++) {
3994
- let lut = this.colormap(this.volumes[i].colorMap);
3995
- let c = new Uint8ClampedArray(luts.length + lut.length);
3996
- c.set(luts);
3997
- c.set(lut, luts.length);
3998
- luts = c;
3999
- } //colorMap
4000
- }
4177
+ function addColormap(lut) {
4178
+ let c = new Uint8ClampedArray(luts.length + lut.length);
4179
+ c.set(luts);
4180
+ c.set(lut, luts.length);
4181
+ luts = c;
4182
+ }
4183
+ for (let i = 1; i < nLayer; i++)
4184
+ addColormap(this.colormap(this.volumes[i].colorMap));
4185
+ //slots for (optional) negative colorMaps
4186
+ let bogusLut = new Uint8ClampedArray(1024);
4187
+ //bogus LUT key - allow shader to discriminate slots used for colorMapNegative
4188
+ bogusLut[0] = 255; //R
4189
+ bogusLut[1] = 255; //G
4190
+ bogusLut[2] = 255; //B
4191
+ bogusLut[3] = 0; //A
4192
+ bogusLut[4] = 0; //R
4193
+ bogusLut[5] = 0; //G
4194
+ bogusLut[6] = 0; //B
4195
+ bogusLut[7] = 255; //A
4196
+ for (let i = 0; i < nLayer; i++) {
4197
+ let lut = bogusLut.slice();
4198
+ if (this.volumes[i].colorMapNegative.length > 0)
4199
+ lut = this.colormap(this.volumes[i].colorMapNegative);
4200
+ addColormap(lut);
4201
+ } //each layer
4001
4202
  this.gl.texSubImage2D(
4002
4203
  this.gl.TEXTURE_2D,
4003
4204
  0,
4004
4205
  0,
4005
4206
  0,
4006
4207
  256,
4007
- nLayer,
4208
+ nLayer * 2,
4008
4209
  this.gl.RGBA,
4009
4210
  this.gl.UNSIGNED_BYTE,
4010
4211
  luts
@@ -4014,23 +4215,64 @@ Niivue.prototype.refreshColormaps = function () {
4014
4215
 
4015
4216
  // not included in public docs
4016
4217
  Niivue.prototype.sliceScale = function () {
4017
- var dims = [
4018
- 1.0,
4019
- this.back.dims[1] * this.back.pixDims[1],
4020
- this.back.dims[2] * this.back.pixDims[2],
4021
- this.back.dims[3] * this.back.pixDims[3],
4022
- ];
4023
- var longestAxis = Math.max(dims[1], Math.max(dims[2], dims[3]));
4218
+ let dimsMM = this.screenFieldOfViewMM(this.sliceTypeAxial);
4219
+ var longestAxis = Math.max(dimsMM[0], Math.max(dimsMM[1], dimsMM[2]));
4024
4220
  var volScale = [
4025
- dims[1] / longestAxis,
4026
- dims[2] / longestAxis,
4027
- dims[3] / longestAxis,
4221
+ dimsMM[0] / longestAxis,
4222
+ dimsMM[1] / longestAxis,
4223
+ dimsMM[2] / longestAxis,
4028
4224
  ];
4029
4225
  var vox = [this.back.dims[1], this.back.dims[2], this.back.dims[3]];
4030
- return { volScale, vox };
4226
+ return { volScale, vox, longestAxis, dimsMM };
4031
4227
  }; // sliceScale()
4032
4228
 
4033
- // not included in public docs
4229
+ function swizzleVec3(vec, order = [0, 1, 2]) {
4230
+ let vout = mat.vec3.create();
4231
+ vout[0] = vec[order[0]];
4232
+ vout[1] = vec[order[1]];
4233
+ vout[2] = vec[order[2]];
4234
+ return vout;
4235
+ }
4236
+
4237
+ Niivue.prototype.inRenderTile = function (x, y) {
4238
+ for (let i = 0; i < this.screenSlices.length; i++) {
4239
+ if (this.screenSlices[i].axCorSag !== this.sliceTypeRender) continue;
4240
+ let ltwh = this.screenSlices[i].leftTopWidthHeight;
4241
+ if (
4242
+ x > ltwh[0] &&
4243
+ y > ltwh[1] &&
4244
+ x < ltwh[0] + ltwh[2] &&
4245
+ y < ltwh[1] + ltwh[3]
4246
+ )
4247
+ return i;
4248
+ }
4249
+ return -1; //mouse position not in rendering tile
4250
+ };
4251
+
4252
+ Niivue.prototype.sliceScroll3D = function (posChange = 0) {
4253
+ if (posChange === 0) return;
4254
+ //n.b. clip plane only influences voxel-based volumes, so zoom is only action for meshes
4255
+ if (this.volumes.length > 0 && this.scene.clipPlaneDepthAziElev[0] < 1.8) {
4256
+ //clipping mode: change clip plane depth
4257
+ //if (this.scene.clipPlaneDepthAziElev[0] > 1.8) return;
4258
+ let depthAziElev = this.scene.clipPlaneDepthAziElev.slice();
4259
+ //bound clip sqrt(3) = 1.73
4260
+ if (posChange > 0) depthAziElev[0] = Math.min(1.5, depthAziElev[0] + 0.025);
4261
+ if (posChange < 0)
4262
+ depthAziElev[0] = Math.max(-1.5, depthAziElev[0] - 0.025); //Math.max(-1.7,
4263
+ if (depthAziElev[0] !== this.scene.clipPlaneDepthAziElev[0]) {
4264
+ this.scene.clipPlaneDepthAziElev = depthAziElev;
4265
+ return this.setClipPlane(this.scene.clipPlaneDepthAziElev);
4266
+ }
4267
+ return;
4268
+ }
4269
+ if (posChange > 0)
4270
+ this.volScaleMultiplier = Math.min(2.0, this.volScaleMultiplier * 1.1);
4271
+ if (posChange < 0)
4272
+ this.volScaleMultiplier = Math.max(0.5, this.volScaleMultiplier * 0.9);
4273
+ this.drawScene();
4274
+ };
4275
+
4034
4276
  Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
4035
4277
  var posNow;
4036
4278
  var posFuture;
@@ -4065,34 +4307,14 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
4065
4307
  return;
4066
4308
  }
4067
4309
  }
4068
- if (this.sliceType === this.sliceTypeRender) {
4069
- if (posChange === 0) return;
4070
- //n.b. clip plane only influences voxel-based volumes, so zoom is only action for meshes
4071
-
4072
- if (this.volumes.length > 0 && this.scene.clipPlaneDepthAziElev[0] < 1.8) {
4073
- //clipping mode: change clip plane depth
4074
- //if (this.scene.clipPlaneDepthAziElev[0] > 1.8) return;
4075
- let depthAziElev = this.scene.clipPlaneDepthAziElev.slice();
4076
- //bound clip sqrt(3) = 1.73
4077
- if (posChange > 0)
4078
- depthAziElev[0] = Math.min(1.5, depthAziElev[0] + 0.025);
4079
- if (posChange < 0)
4080
- depthAziElev[0] = Math.max(-1.5, depthAziElev[0] - 0.025); //Math.max(-1.7,
4081
- if (depthAziElev[0] !== this.scene.clipPlaneDepthAziElev[0]) {
4082
- this.scene.clipPlaneDepthAziElev = depthAziElev;
4083
- return this.setClipPlane(this.scene.clipPlaneDepthAziElev);
4084
- }
4085
- return;
4086
- }
4087
- if (posChange > 0)
4088
- this.volScaleMultiplier = Math.min(2.0, this.volScaleMultiplier * 1.1);
4089
- if (posChange < 0)
4090
- this.volScaleMultiplier = Math.max(0.5, this.volScaleMultiplier * 0.9);
4091
- this.drawScene();
4310
+ //if (this.sliceType === this.sliceTypeRender) {
4311
+ if (this.inRenderTile(x, y) >= 0) {
4312
+ this.sliceScroll3D(posChange);
4313
+ this.drawScene(); //TODO: twice?
4092
4314
  return;
4093
4315
  }
4094
4316
  if (
4095
- this.numScreenSlices < 1 ||
4317
+ this.screenSlices.length < 1 ||
4096
4318
  this.gl.canvas.height < 1 ||
4097
4319
  this.gl.canvas.width < 1
4098
4320
  )
@@ -4101,22 +4323,15 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
4101
4323
  // webGL clip space L,R,T,B = [-1, 1, 1, 1]
4102
4324
  // n.b. webGL Y polarity reversed
4103
4325
  // https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
4104
- for (let i = 0; i < this.numScreenSlices; i++) {
4326
+ for (let i = 0; i < this.screenSlices.length; i++) {
4105
4327
  var axCorSag = this.screenSlices[i].axCorSag;
4106
4328
  if (this.drawPenAxCorSag >= 0 && this.drawPenAxCorSag !== axCorSag)
4107
4329
  continue; //if mouse is drawing on axial slice, ignore any entry to coronal slice
4108
4330
  if (axCorSag > this.sliceTypeSagittal) continue;
4109
- var ltwh = this.screenSlices[i].leftTopWidthHeight;
4110
- let isMirror = false;
4111
- if (ltwh[2] < 0) {
4112
- isMirror = true;
4113
- ltwh[0] += ltwh[2];
4114
- ltwh[2] = -ltwh[2];
4115
- }
4116
- var fracX = (x - ltwh[0]) / ltwh[2];
4117
- if (isMirror) fracX = 1.0 - fracX;
4118
- var fracY = 1.0 - (y - ltwh[1]) / ltwh[3];
4119
- if (fracX >= 0.0 && fracX < 1.0 && fracY >= 0.0 && fracY < 1.0) {
4331
+ let texFrac = this.screenXY2TextureFrac(x, y, i);
4332
+ if (texFrac[0] < 0) continue; //click not on slice i
4333
+
4334
+ if (true) {
4120
4335
  //user clicked on slice i
4121
4336
  if (!isDelta) {
4122
4337
  this.scene.crosshairPos[2 - axCorSag] = posChange;
@@ -4131,31 +4346,21 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
4131
4346
  if (posFuture < 0) posFuture = 0;
4132
4347
  this.scene.crosshairPos[2 - axCorSag] = posFuture;
4133
4348
  this.drawScene();
4134
- this.scene.location$.next({
4349
+ this.opts.onLocationChange({
4135
4350
  mm: this.frac2mm(this.scene.crosshairPos),
4136
4351
  vox: this.frac2vox(this.scene.crosshairPos),
4137
4352
  frac: this.scene.crosshairPos,
4353
+ xy: [x, y],
4138
4354
  values: this.volumes.map((v) => {
4139
4355
  let mm = this.frac2mm(this.scene.crosshairPos);
4140
4356
  let vox = v.mm2vox(mm);
4141
4357
  let val = v.getValue(...vox);
4142
- return val;
4358
+ return { name: v.name, value: val, id: v.id, mm: mm, vox: vox };
4143
4359
  }),
4144
4360
  });
4145
4361
  return;
4146
4362
  }
4147
- if (axCorSag === this.sliceTypeAxial) {
4148
- this.scene.crosshairPos[0] = fracX;
4149
- this.scene.crosshairPos[1] = fracY;
4150
- }
4151
- if (axCorSag === this.sliceTypeCoronal) {
4152
- this.scene.crosshairPos[0] = fracX;
4153
- this.scene.crosshairPos[2] = fracY;
4154
- }
4155
- if (axCorSag === this.sliceTypeSagittal) {
4156
- this.scene.crosshairPos[1] = fracX;
4157
- this.scene.crosshairPos[2] = fracY;
4158
- }
4363
+ this.scene.crosshairPos = texFrac.slice();
4159
4364
  if (this.opts.drawingEnabled) {
4160
4365
  let pt = this.frac2vox(this.scene.crosshairPos);
4161
4366
  if (this.opts.penValue < 0 || Object.is(this.opts.penValue, -0)) {
@@ -4173,23 +4378,23 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
4173
4378
  pt[2] === this.drawPenLocation[2]
4174
4379
  )
4175
4380
  return;
4176
- this.drawLine(pt, this.drawPenLocation, this.opts.penValue);
4381
+ this.drawPenLine(pt, this.drawPenLocation, this.opts.penValue);
4177
4382
  }
4178
4383
  this.drawPenLocation = pt;
4179
4384
  if (this.opts.isFilledPen) this.drawPenFillPts.push(pt);
4180
- //xxxxxx
4181
4385
  this.refreshDrawing(false);
4182
4386
  }
4183
4387
  this.drawScene();
4184
- this.scene.location$.next({
4388
+ this.opts.onLocationChange({
4185
4389
  mm: this.frac2mm(this.scene.crosshairPos),
4186
4390
  vox: this.frac2vox(this.scene.crosshairPos),
4187
4391
  frac: this.scene.crosshairPos,
4392
+ xy: [x, y],
4188
4393
  values: this.volumes.map((v) => {
4189
4394
  let mm = this.frac2mm(this.scene.crosshairPos);
4190
4395
  let vox = v.mm2vox(mm);
4191
4396
  let val = v.getValue(...vox);
4192
- return val;
4397
+ return { name: v.name, value: val, id: v.id, mm: mm, vox: vox };
4193
4398
  }),
4194
4399
  });
4195
4400
  return;
@@ -4205,19 +4410,135 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
4205
4410
  } //for i: each slice on screen
4206
4411
  }; // mouseClick()
4207
4412
 
4208
- // not included in public docs
4209
- Niivue.prototype.drawSelectionBox = function (leftTopWidthHeight) {
4413
+ Niivue.prototype.drawRuler = function () {
4414
+ let fovMM = [];
4415
+ let ltwh = [];
4416
+ //leftTopWidthHeight
4417
+ for (let i = 0; i < this.screenSlices.length; i++) {
4418
+ if (this.screenSlices[i].axCorSag === this.sliceTypeRender) continue;
4419
+ //let ltwh = this.screenSlices[i].leftTopWidthHeight;
4420
+ if (this.screenSlices[i].fovMM.length > 1) {
4421
+ ltwh = this.screenSlices[i].leftTopWidthHeight;
4422
+ fovMM = this.screenSlices[i].fovMM;
4423
+ break;
4424
+ }
4425
+ }
4426
+ if (ltwh.length < 4) return;
4427
+ let frac10cm = 100.0 / fovMM[0];
4428
+ let pix10cm = frac10cm * ltwh[2];
4429
+ let pixLeft = ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm;
4430
+ let pixTop = ltwh[1] + ltwh[3] - 2 * this.opts.rulerWidth;
4431
+ let startXYendXY = [pixLeft, pixTop, pixLeft + pix10cm, pixTop];
4432
+ this.drawRuler10cm(startXYendXY);
4433
+ };
4434
+
4435
+ Niivue.prototype.drawRuler10cm = function (startXYendXY) {
4436
+ this.gl.bindVertexArray(this.genericVAO);
4210
4437
  this.lineShader.use(this.gl);
4211
4438
  this.gl.uniform4fv(
4212
4439
  this.lineShader.uniforms["lineColor"],
4213
- this.opts.selectionBoxColor
4440
+ this.opts.rulerColor
4214
4441
  );
4215
4442
  this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
4216
4443
  this.gl.canvas.width,
4217
4444
  this.gl.canvas.height,
4218
4445
  ]);
4446
+ //draw Line
4447
+ this.gl.uniform1f(
4448
+ this.lineShader.uniforms["thickness"],
4449
+ this.opts.rulerWidth
4450
+ );
4451
+ this.gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], startXYendXY);
4452
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
4453
+ //draw tick marks
4454
+ let w1cm = -0.1 * (startXYendXY[0] - startXYendXY[2]);
4455
+ let b = startXYendXY[1];
4456
+ let t = b - 2 * this.opts.rulerWidth;
4457
+ let t2 = b - 4 * this.opts.rulerWidth;
4458
+ for (let i = 0; i < 11; i++) {
4459
+ let l = startXYendXY[0] + i * w1cm;
4460
+ let xyxy = [l, b, l, t];
4461
+ if (i % 5 === 0) xyxy[3] = t2;
4462
+ this.gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], xyxy);
4463
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
4464
+ }
4465
+
4466
+ this.gl.bindVertexArray(this.unusedVAO); //set vertex attributes
4467
+ };
4468
+
4469
+ Niivue.prototype.drawMeasurementTool = function (startXYendXY) {
4470
+ let gl = this.gl;
4471
+ gl.bindVertexArray(this.genericVAO);
4472
+
4473
+ gl.depthFunc(gl.ALWAYS);
4474
+ gl.enable(gl.BLEND);
4475
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
4476
+
4477
+ this.lineShader.use(this.gl);
4478
+ gl.uniform4fv(this.lineShader.uniforms["lineColor"], this.opts.rulerColor);
4479
+ gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
4480
+ gl.canvas.width,
4481
+ gl.canvas.height,
4482
+ ]);
4483
+ //draw Line
4484
+ gl.uniform1f(this.lineShader.uniforms["thickness"], this.opts.rulerWidth);
4485
+ gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], startXYendXY);
4486
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
4487
+ //draw startCap
4488
+ let c = this.opts.rulerColor;
4489
+ c[3] = 1.0; //opaque
4490
+ gl.uniform4fv(this.lineShader.uniforms["lineColor"], c);
4491
+ let w = this.opts.rulerWidth;
4492
+ gl.uniform1f(this.lineShader.uniforms["thickness"], w * 2);
4493
+ let sXYeXY = [
4494
+ startXYendXY[0],
4495
+ startXYendXY[1] - w,
4496
+ startXYendXY[0],
4497
+ startXYendXY[1] + w,
4498
+ ];
4499
+ gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], sXYeXY);
4500
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
4501
+ //end cap
4502
+ sXYeXY = [
4503
+ startXYendXY[2],
4504
+ startXYendXY[3] - w,
4505
+ startXYendXY[2],
4506
+ startXYendXY[3] + w,
4507
+ ];
4508
+ gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], sXYeXY);
4509
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
4510
+ //distance between start and stop
4511
+ let startXY = this.canvasPos2frac([startXYendXY[0], startXYendXY[1]]);
4512
+ let endXY = this.canvasPos2frac([startXYendXY[2], startXYendXY[3]]);
4513
+ if (startXY[0] >= 0 && endXY[0] >= 0) {
4514
+ startXY = this.frac2mm(startXY);
4515
+ endXY = this.frac2mm(endXY);
4516
+ let v = mat.vec3.create();
4517
+ mat.vec3.sub(v, startXY, endXY);
4518
+ let lenMM = mat.vec3.len(v);
4519
+ let decimals = 2;
4520
+ if (lenMM > 9) decimals = 1;
4521
+ if (lenMM > 99) decimals = 0;
4522
+ let stringMM = lenMM.toFixed(decimals);
4523
+ this.drawTextBetween(startXYendXY, stringMM);
4524
+ }
4525
+ gl.bindVertexArray(this.unusedVAO); //set vertex attributes
4526
+ };
4527
+
4528
+ // not included in public docs
4529
+ Niivue.prototype.drawBox = function (
4530
+ leftTopWidthHeight,
4531
+ lineColor = [1, 0, 0, 1]
4532
+ ) {
4533
+ this.rectShader.use(this.gl);
4534
+ this.gl.enable(this.gl.BLEND);
4535
+ this.gl.uniform4fv(this.rectShader.uniforms["lineColor"], lineColor);
4536
+ this.gl.uniform2fv(this.rectShader.uniforms["canvasWidthHeight"], [
4537
+ this.gl.canvas.width,
4538
+ this.gl.canvas.height,
4539
+ ]);
4219
4540
  this.gl.uniform4f(
4220
- this.lineShader.uniforms["leftTopWidthHeight"],
4541
+ this.rectShader.uniforms["leftTopWidthHeight"],
4221
4542
  leftTopWidthHeight[0],
4222
4543
  leftTopWidthHeight[1],
4223
4544
  leftTopWidthHeight[2],
@@ -4228,18 +4549,103 @@ Niivue.prototype.drawSelectionBox = function (leftTopWidthHeight) {
4228
4549
  this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
4229
4550
  };
4230
4551
 
4231
- // not included in public docs
4232
- Niivue.prototype.drawColorbar = function (leftTopWidthHeight) {
4552
+ Niivue.prototype.drawSelectionBox = function (
4553
+ leftTopWidthHeight,
4554
+ lineColor = [1, 0, 0, 1]
4555
+ ) {
4556
+ this.drawBox(leftTopWidthHeight, this.opts.selectionBoxColor);
4557
+ };
4558
+
4559
+ function tickSpacingX(tickCount, mn, mx) {
4560
+ //https://www.realtimerendering.com/resources/GraphicsGems/gems/Label.c
4561
+ //https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
4562
+ let range = Math.abs(mx - mn);
4563
+ if (range === 0.0) return [0, 0];
4564
+ let unroundedTickSize = range / (tickCount - 1);
4565
+ let x = Math.ceil(Math.log10(unroundedTickSize) - 1);
4566
+ let pow10x = Math.pow(10, x);
4567
+ let spacing = Math.ceil(unroundedTickSize / pow10x) * pow10x;
4568
+ let ticMin = mn;
4569
+ if (ticMin % spacing !== 0.0 && range % spacing !== 0.0)
4570
+ ticMin = Math.floor((mn + spacing) / spacing) * spacing;
4571
+ if ((mn / spacing) % 1 !== 0.0)
4572
+ ticMin = Math.sign(ticMin) * Math.round(Math.abs(ticMin));
4573
+ return [spacing, ticMin];
4574
+ }
4575
+
4576
+ function tickSpacing(mn, mx) {
4577
+ let range = Math.abs(mx - mn);
4578
+ let [spacing, ticMin] = tickSpacingX(5, mn, mx);
4579
+ if (range % spacing === 0) return [spacing, ticMin];
4580
+ //[spacing, ticMin] = tickSpacingX(6, mn, mx);
4581
+ //if ((range % spacing) === 0) return [spacing, ticMin];
4582
+ [spacing, ticMin] = tickSpacingX(4, mn, mx);
4583
+ if (range % spacing === 0) return [spacing, ticMin];
4584
+ [spacing, ticMin] = tickSpacingX(5, mn, mx);
4585
+ return [spacing, ticMin];
4586
+ }
4587
+
4588
+ Niivue.prototype.effectiveCanvasHeight = function () {
4589
+ //available canvas height differs from actual height if bottom colorbar is shown
4590
+ return this.gl.canvas.height - this.colorbarHeight;
4591
+ };
4592
+
4593
+ Niivue.prototype.reserveColorbarPanel = function () {
4594
+ let txtHt = Math.max(this.opts.textHeight, 0.01);
4595
+ txtHt = txtHt * Math.min(this.gl.canvas.height, this.gl.canvas.width);
4596
+ let margin = txtHt;
4597
+ let fullHt = 3 * txtHt;
4598
+ let leftTopWidthHeight = [
4599
+ 0,
4600
+ this.gl.canvas.height - fullHt,
4601
+ this.gl.canvas.width,
4602
+ fullHt,
4603
+ ];
4604
+ this.colorbarHeight = leftTopWidthHeight[3] + 1;
4605
+ return leftTopWidthHeight;
4606
+ };
4607
+
4608
+ Niivue.prototype.drawColorbarCore = function (
4609
+ layer = 0,
4610
+ leftTopWidthHeight = [0, 0, 0, 0],
4611
+ isNegativeColor = false
4612
+ ) {
4613
+ if (this.volumes.length < 1) return;
4614
+ if (layer >= this.volumes.length) layer = 0;
4233
4615
  if (leftTopWidthHeight[2] <= 0 || leftTopWidthHeight[3] <= 0) return;
4234
- // if (this.opts.crosshairWidth > 0) {
4235
- // //gl.disable(gl.DEPTH_TEST);
4236
- // this.lineShader.use(this.gl)
4237
- // this.gl.uniform4fv(this.lineShader.uniforms["lineColor"], this.opts.crosshairColor);
4238
- // this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [this.gl.canvas.width, this.gl.canvas.height]);
4239
- // let ltwh = [leftTopWidthHeight[0]-1, leftTopWidthHeight[1]-1, leftTopWidthHeight[2]+2, leftTopWidthHeight[3]+2];
4240
- // this.gl.uniform4f(this.lineShader.uniforms["leftTopWidthHeight"], ltwh[0], ltwh[1], ltwh[2], ltwh[3]);
4241
- // this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
4242
- // }
4616
+ let txtHt = Math.max(this.opts.textHeight, 0.01);
4617
+ txtHt = txtHt * Math.min(this.gl.canvas.height, this.gl.canvas.width);
4618
+ let margin = txtHt;
4619
+ let fullHt = 3 * txtHt;
4620
+ let barHt = txtHt;
4621
+ if (leftTopWidthHeight[3] < fullHt) {
4622
+ //no space for text
4623
+ if (leftTopWidthHeight[3] < 3) return;
4624
+ margin = 1;
4625
+ barHt = leftTopWidthHeight[3] - 2;
4626
+ }
4627
+ this.gl.disable(this.gl.DEPTH_TEST);
4628
+ this.colorbarHeight = leftTopWidthHeight[3] + 1;
4629
+ let clr = [
4630
+ this.opts.backColor[0],
4631
+ this.opts.backColor[1],
4632
+ this.opts.backColor[2],
4633
+ 0.5,
4634
+ ];
4635
+ let top = leftTopWidthHeight[1];
4636
+ let barLTWH = [
4637
+ leftTopWidthHeight[0] + margin,
4638
+ leftTopWidthHeight[1],
4639
+ leftTopWidthHeight[2] - 2 * margin,
4640
+ barHt,
4641
+ ];
4642
+ let rimLTWH = [
4643
+ barLTWH[0] - 1,
4644
+ barLTWH[1] - 1,
4645
+ barLTWH[2] + 2,
4646
+ barLTWH[3] + 2,
4647
+ ];
4648
+ this.drawBox(rimLTWH, this.opts.crosshairColor);
4243
4649
  this.colorbarShader.use(this.gl);
4244
4650
  this.gl.activeTexture(this.gl.TEXTURE1);
4245
4651
  this.gl.bindTexture(this.gl.TEXTURE_2D, this.colormapTexture);
@@ -4253,16 +4659,16 @@ Niivue.prototype.drawColorbar = function (leftTopWidthHeight) {
4253
4659
  this.gl.TEXTURE_MAG_FILTER,
4254
4660
  this.gl.NEAREST
4255
4661
  );
4662
+ let lx = layer;
4663
+ if (isNegativeColor) lx += this.volumes.length;
4664
+ this.gl.uniform1f(this.colorbarShader.uniforms["layer"], lx);
4256
4665
  this.gl.uniform2fv(this.colorbarShader.uniforms["canvasWidthHeight"], [
4257
4666
  this.gl.canvas.width,
4258
4667
  this.gl.canvas.height,
4259
4668
  ]);
4260
- this.gl.uniform4f(
4669
+ this.gl.uniform4fv(
4261
4670
  this.colorbarShader.uniforms["leftTopWidthHeight"],
4262
- leftTopWidthHeight[0],
4263
- leftTopWidthHeight[1],
4264
- leftTopWidthHeight[2],
4265
- leftTopWidthHeight[3]
4671
+ barLTWH
4266
4672
  );
4267
4673
  this.gl.bindVertexArray(this.genericVAO);
4268
4674
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
@@ -4277,8 +4683,55 @@ Niivue.prototype.drawColorbar = function (leftTopWidthHeight) {
4277
4683
  this.gl.TEXTURE_MAG_FILTER,
4278
4684
  this.gl.LINEAR
4279
4685
  );
4280
- //gl.enable(gl.DEPTH_TEST);
4281
- }; // drawColorbar()
4686
+ let min = this.volumes[layer].cal_min;
4687
+ let max = this.volumes[layer].cal_max;
4688
+ if (min >= max || txtHt < 1) return;
4689
+ let range = max - min;
4690
+ let [spacing, ticMin] = tickSpacing(min, max);
4691
+ //determine font size
4692
+ function humanize(x) {
4693
+ //drop trailing zeros from numerical string
4694
+ return x.toFixed(6).replace(/\.?0*$/, "");
4695
+ }
4696
+ let tic = ticMin;
4697
+ let ticLTWH = [0, barLTWH[1] + barLTWH[3] - txtHt * 0.5, 2, txtHt * 0.75];
4698
+ let txtTop = ticLTWH[1] + ticLTWH[3];
4699
+ while (tic <= max) {
4700
+ ticLTWH[0] = barLTWH[0] + ((tic - min) / range) * barLTWH[2];
4701
+ this.drawBox(ticLTWH);
4702
+ let str = humanize(tic);
4703
+ if (isNegativeColor) str = "-" + str;
4704
+ //if (fntSize > 0)
4705
+ this.drawTextBelow([ticLTWH[0], txtTop], str);
4706
+ //this.drawTextRight([plotLTWH[0], y], str, fntScale)
4707
+ tic += spacing;
4708
+ }
4709
+ };
4710
+
4711
+ Niivue.prototype.drawColorbar = function () {
4712
+ let nlayers = this.volumes.length;
4713
+ if (nlayers < 1) return;
4714
+ let ncolorMapNegative = 0;
4715
+ for (let i = 0; i < nlayers; i++)
4716
+ if (this.volumes[i].colorMapNegative.length > 0) ncolorMapNegative++;
4717
+ let leftTopWidthHeight = this.reserveColorbarPanel();
4718
+ let txtHt = Math.max(this.opts.textHeight, 0.01);
4719
+ txtHt = txtHt * Math.min(this.gl.canvas.height, this.gl.canvas.width);
4720
+ let fullHt = 3 * txtHt;
4721
+ let wid = leftTopWidthHeight[2] / (nlayers + ncolorMapNegative);
4722
+ if (leftTopWidthHeight[2] <= 0 || leftTopWidthHeight[3] <= 0) {
4723
+ wid = this.gl.canvas.width / nlayers;
4724
+ leftTopWidthHeight = [0, this.gl.canvas.height - fullHt, wid, fullHt];
4725
+ }
4726
+ leftTopWidthHeight[2] = wid;
4727
+ for (let i = 0; i < nlayers; i++) {
4728
+ this.drawColorbarCore(i, leftTopWidthHeight);
4729
+ leftTopWidthHeight[0] += wid;
4730
+ if (this.volumes[i].colorMapNegative.length === 0) continue;
4731
+ this.drawColorbarCore(i, leftTopWidthHeight, true);
4732
+ leftTopWidthHeight[0] += wid;
4733
+ }
4734
+ };
4282
4735
 
4283
4736
  // not included in public docs
4284
4737
  Niivue.prototype.textWidth = function (scale, str) {
@@ -4360,7 +4813,48 @@ Niivue.prototype.drawTextLeft = function (xy, str, scale = 1, color = null) {
4360
4813
  xy[0] -= this.textWidth(size, str);
4361
4814
  xy[1] -= 0.5 * size;
4362
4815
  this.drawText(xy, str, scale, color);
4363
- }; // drawTextRight()
4816
+ }; // drawTextLeft()
4817
+
4818
+ // not included in public docs
4819
+ Niivue.prototype.drawTextRightBelow = function (
4820
+ xy,
4821
+ str,
4822
+ scale = 1,
4823
+ color = null
4824
+ ) {
4825
+ //to right of x, vertically centered on y
4826
+ if (this.opts.textHeight <= 0) return;
4827
+ let size = this.opts.textHeight * this.gl.canvas.height * scale;
4828
+ //xy[0] -= this.textWidth(size, str);
4829
+ //xy[1] -= 0.5 * size;
4830
+ this.drawText(xy, str, scale, color);
4831
+ }; // drawTextLeftBelow()
4832
+
4833
+ // not included in public docs
4834
+ Niivue.prototype.drawTextBetween = function (
4835
+ startXYendXY,
4836
+ str,
4837
+ scale = 1,
4838
+ color = null
4839
+ ) {
4840
+ //horizontally centered on x, below y
4841
+ if (this.opts.textHeight <= 0) return;
4842
+ let xy = [
4843
+ (startXYendXY[0] + startXYendXY[2]) * 0.5,
4844
+ (startXYendXY[1] + startXYendXY[3]) * 0.5,
4845
+ ];
4846
+ let size = this.opts.textHeight * this.gl.canvas.height * scale;
4847
+ let w = this.textWidth(size, str);
4848
+ xy[0] -= 0.5 * w;
4849
+ xy[1] -= 0.5 * size;
4850
+ let LTWH = [xy[0] - 1, xy[1] - 1, w + 2, size + 2];
4851
+ let clr = color;
4852
+ if (clr === null) clr = this.opts.crosshairColor;
4853
+ if (clr[0] + clr[1] + clr[2] > 0.8) clr = [0, 0, 0, 0.5];
4854
+ else clr = [1, 1, 1, 0.5];
4855
+ this.drawRect(LTWH, clr);
4856
+ this.drawText(xy, str, scale, color);
4857
+ }; // drawTextBetween()
4364
4858
 
4365
4859
  // not included in public docs
4366
4860
  Niivue.prototype.drawTextBelow = function (xy, str, scale = 1, color = null) {
@@ -4396,187 +4890,382 @@ Niivue.prototype.setInterpolation = function (isNearest) {
4396
4890
  this.drawScene();
4397
4891
  }; // setInterpolation()
4398
4892
 
4399
- Niivue.prototype.calculateMvpMatrixX = function (leftTopWidthHeight) {
4893
+ function deg2rad(deg) {
4894
+ return deg * (Math.PI / 180.0);
4895
+ } // deg2rad()
4896
+
4897
+ Niivue.prototype.calculateMvpMatrix2D = function (
4898
+ leftTopWidthHeight,
4899
+ mn,
4900
+ mx,
4901
+ clipTolerance = Infinity,
4902
+ clipDepth = 0,
4903
+ azimuth = null,
4904
+ elevation = null,
4905
+ isRadiolgical
4906
+ ) {
4400
4907
  let gl = this.gl;
4401
4908
  gl.viewport(
4402
4909
  leftTopWidthHeight[0],
4403
- leftTopWidthHeight[1],
4910
+ this.gl.canvas.clientHeight -
4911
+ (leftTopWidthHeight[1] + leftTopWidthHeight[3]), //lower numbers near bottom
4404
4912
  leftTopWidthHeight[2],
4405
4913
  leftTopWidthHeight[3]
4406
4914
  );
4407
- let whratio = leftTopWidthHeight[2] / leftTopWidthHeight[3];
4408
- //pivot from center of objects
4409
- //let scale = this.furthestVertexFromOrigin;
4410
- //let origin = [0,0,0];
4411
- console.log(">>", this.back.dimsRAS, this.back.matRAS);
4412
-
4413
- let scale = this.furthestFromPivot * 0.5;
4414
-
4415
- let origin = [0, 0, 0];
4915
+ let left = mn[0];
4916
+ let right = mx[0];
4917
+ let leftTopMM = [left, mn[1]];
4918
+ let fovMM = [right - left, mx[1] - mn[1]];
4919
+ if (isRadiolgical) {
4920
+ leftTopMM = [mx[0], mn[1]];
4921
+ fovMM = [mn[0] - mx[0], mx[1] - mn[1]];
4922
+ left = -mx[0];
4923
+ right = -mn[0];
4924
+ }
4925
+ let scale = 2 * Math.max(Math.abs(mn[2]), Math.abs(mx[2])); //3rd dimension is near/far from camera
4416
4926
  let projectionMatrix = mat.mat4.create();
4417
- //scale = (0.8 * scale) / this.volScaleMultiplier; //2.0 WebGL viewport has range of 2.0 [-1,-1]...[1,1]
4418
- if (whratio < 1)
4419
- //tall window: "portrait" mode, width constrains
4420
- mat.mat4.ortho(
4421
- projectionMatrix,
4422
- -scale,
4423
- scale,
4424
- -scale / whratio,
4425
- scale / whratio,
4426
- 0.01,
4427
- scale * 8.0
4428
- );
4429
- //Wide window: "landscape" mode, height constrains
4430
- else
4431
- mat.mat4.ortho(
4432
- projectionMatrix,
4433
- -scale * whratio,
4434
- scale * whratio,
4435
- -scale,
4436
- scale,
4437
- 0.01,
4438
- scale * 8.0
4439
- );
4927
+ let near = 0.01;
4928
+ let far = scale * 8.0;
4929
+ if (clipTolerance !== Infinity) {
4930
+ let r = isRadiolgical;
4931
+ if (elevation === 0 && (azimuth === 0 || azimuth === 180)) r = !r;
4932
+ let dx = scale * 1.8 - clipDepth;
4933
+ if (!r) dx = scale * 1.8 + clipDepth;
4934
+ near = dx - clipTolerance;
4935
+ far = dx + clipTolerance;
4936
+ }
4937
+ mat.mat4.ortho(projectionMatrix, left, right, mn[1], mx[1], near, far);
4440
4938
  const modelMatrix = mat.mat4.create();
4441
4939
  modelMatrix[0] = -1; //mirror X coordinate
4442
4940
  //push the model away from the camera so camera not inside model
4443
-
4444
4941
  let translateVec3 = mat.vec3.fromValues(0, 0, -scale * 1.8); // to avoid clipping, >= SQRT(3)
4445
4942
  mat.mat4.translate(modelMatrix, modelMatrix, translateVec3);
4446
- if (this.position)
4447
- mat.mat4.translate(modelMatrix, modelMatrix, this.position);
4448
-
4449
- mat.mat4.translate(modelMatrix, modelMatrix, [
4450
- -origin[0],
4451
- -origin[1],
4452
- -origin[2],
4453
- ]);
4454
- //
4943
+ //apply elevation
4944
+ mat.mat4.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
4945
+ //apply azimuth
4946
+ mat.mat4.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
4455
4947
  let iModelMatrix = mat.mat4.create();
4456
4948
  mat.mat4.invert(iModelMatrix, modelMatrix);
4457
4949
  let normalMatrix = mat.mat4.create();
4458
4950
  mat.mat4.transpose(normalMatrix, iModelMatrix);
4459
- //this.gl.uniformMatrix4fv(this.meshShader.uniforms["mvpMtx"], false, m);
4460
-
4461
- // transpose(out, a) - > {mat4}
4462
- // invert(out, a)
4463
- // normalMatrix := modelMatrix.Inverse.Transpose;
4464
4951
  let modelViewProjectionMatrix = mat.mat4.create();
4465
4952
  mat.mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
4466
- return [modelViewProjectionMatrix, modelMatrix, normalMatrix];
4467
- }; // calculateMvpMatrixX
4468
4953
 
4469
- Niivue.prototype.drawMesh3DX = function (
4470
- isDepthTest = true,
4471
- alpha = 1.0,
4472
- leftTopWidthHeight
4954
+ return {
4955
+ modelViewProjectionMatrix,
4956
+ modelMatrix,
4957
+ normalMatrix,
4958
+ leftTopMM,
4959
+ fovMM,
4960
+ };
4961
+ }; // calculateMvpMatrix2D
4962
+
4963
+ Niivue.prototype.swizzleVec3MM = function (v3, axCorSag) {
4964
+ // change order of vector components
4965
+ if (axCorSag === this.sliceTypeCoronal) {
4966
+ //2D coronal screenXYZ = nifti [i,k,j]
4967
+ v3 = swizzleVec3(v3, [0, 2, 1]);
4968
+ } else if (axCorSag === this.sliceTypeSagittal) {
4969
+ //2D sagittal screenXYZ = nifti [j,k,i]
4970
+ v3 = swizzleVec3(v3, [1, 2, 0]);
4971
+ }
4972
+ return v3;
4973
+ }; // swizzleVec3MM()
4974
+
4975
+ Niivue.prototype.screenFieldOfViewMM = function (
4976
+ axCorSag = 0,
4977
+ forceSliceMM = false
4473
4978
  ) {
4474
- if (this.meshes.length < 1) return;
4475
- let gl = this.gl;
4476
- let m, modelMtx, normMtx;
4477
- [m, modelMtx, normMtx] = this.calculateMvpMatrixX(leftTopWidthHeight);
4979
+ //extent of volume/mesh (in millimeters) in screen space
4980
+ if (!forceSliceMM && !this.opts.isSliceMM) {
4981
+ //return voxel space
4982
+ let fov = this.volumeObject3D.fieldOfViewDeObliqueMM.slice();
4983
+ return this.swizzleVec3MM(fov, axCorSag);
4984
+ }
4985
+ let mnMM = this.volumeObject3D.extentsMin.slice();
4986
+ let mxMM = this.volumeObject3D.extentsMax.slice();
4987
+ let rotation = mat.mat4.create(); //identity matrix: 2D axial screenXYZ = nifti [i,j,k]
4988
+ mnMM = this.swizzleVec3MM(mnMM, axCorSag);
4989
+ mxMM = this.swizzleVec3MM(mxMM, axCorSag);
4990
+ let fovMM = mat.vec3.create();
4991
+ mat.vec3.subtract(fovMM, mxMM, mnMM);
4992
+ return fovMM;
4993
+ };
4478
4994
 
4479
- gl.enable(gl.DEPTH_TEST);
4480
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
4995
+ Niivue.prototype.screenFieldOfViewExtendedMM = function (axCorSag = 0) {
4996
+ //extent of volume/mesh (in millimeters) in screen space
4997
+ let mnMM = this.volumeObject3D.extentsMin.slice();
4998
+ let mxMM = this.volumeObject3D.extentsMax.slice();
4999
+ let rotation = mat.mat4.create(); //identity matrix: 2D axial screenXYZ = nifti [i,j,k]
5000
+ mnMM = this.swizzleVec3MM(mnMM, axCorSag);
5001
+ mxMM = this.swizzleVec3MM(mxMM, axCorSag);
5002
+ let fovMM = mat.vec3.create();
5003
+ mat.vec3.subtract(fovMM, mxMM, mnMM);
5004
+ return { mnMM, mxMM, rotation, fovMM };
5005
+ };
4481
5006
 
4482
- gl.disable(gl.BLEND);
4483
- gl.depthFunc(gl.GREATER);
5007
+ Niivue.prototype.drawSliceOrientationText = function (
5008
+ leftTopWidthHeight,
5009
+ axCorSag
5010
+ ) {
5011
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
5012
+ let topText = "S";
4484
5013
 
4485
- if (isDepthTest) {
4486
- gl.disable(gl.BLEND);
4487
- gl.depthFunc(gl.GREATER);
4488
- } else {
4489
- gl.enable(gl.BLEND);
4490
- gl.depthFunc(gl.ALWAYS);
5014
+ if (axCorSag === this.sliceTypeAxial) topText = "A";
5015
+ let leftText = this.opts.isRadiologicalConvention ? "R" : "L";
5016
+ if (axCorSag === this.sliceTypeSagittal)
5017
+ leftText = this.opts.sagittalNoseLeft ? "A" : "P";
5018
+ if (this.opts.isCornerOrientationText) {
5019
+ this.drawTextRightBelow(
5020
+ [leftTopWidthHeight[0], leftTopWidthHeight[1]],
5021
+ leftText + topText
5022
+ );
5023
+ return;
4491
5024
  }
4492
- gl.disable(gl.CULL_FACE); //show front and back faces
4493
- //Draw the mesh
4494
- this.meshShader.use(this.gl); // set Shader
4495
- //set shader uniforms
4496
- gl.uniformMatrix4fv(this.meshShader.uniforms["mvpMtx"], false, m);
4497
- gl.uniformMatrix4fv(this.meshShader.uniforms["modelMtx"], false, modelMtx);
4498
- gl.uniformMatrix4fv(this.meshShader.uniforms["normMtx"], false, normMtx);
4499
- gl.uniform1f(this.meshShader.uniforms["opacity"], alpha);
4500
- let hasFibers = false;
5025
+ this.drawTextBelow(
5026
+ [
5027
+ leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5,
5028
+ leftTopWidthHeight[1],
5029
+ ],
5030
+ topText
5031
+ );
4501
5032
 
4502
- for (let i = 0; i < this.meshes.length; i++) {
4503
- if (this.meshes[i].indexCount < 3) continue;
4504
- if (this.meshes[i].offsetPt0) {
4505
- hasFibers = true;
4506
- continue;
4507
- }
4508
- gl.bindVertexArray(this.meshes[i].vao);
4509
- gl.drawElements(
4510
- gl.TRIANGLES,
4511
- this.meshes[i].indexCount,
4512
- gl.UNSIGNED_INT,
4513
- 0
4514
- );
4515
- gl.bindVertexArray(this.unusedVAO);
5033
+ this.drawTextRight(
5034
+ [
5035
+ leftTopWidthHeight[0],
5036
+ leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5,
5037
+ ],
5038
+ leftText
5039
+ );
5040
+ };
5041
+
5042
+ Niivue.prototype.xyMM2xyzMM = function (axCorSag, sliceFrac) {
5043
+ //given X and Y, find Z for a plane defined by 3 points (a,b,c)
5044
+ //https://math.stackexchange.com/questions/851742/calculate-coordinate-of-any-point-on-triangle-in-3d-plane
5045
+ let sliceDim = 2; //axial depth is NIfTI k dimension
5046
+ if (axCorSag === this.sliceTypeCoronal) sliceDim = 1; //sagittal depth is NIfTI j dimension
5047
+ if (axCorSag === this.sliceTypeSagittal) sliceDim = 0; //sagittal depth is NIfTI i dimension
5048
+ let a = [0, 0, 0];
5049
+ let b = [1, 1, 0];
5050
+ let c = [1, 0, 1];
5051
+
5052
+ a[sliceDim] = sliceFrac;
5053
+ b[sliceDim] = sliceFrac;
5054
+ c[sliceDim] = sliceFrac;
5055
+ a = this.frac2mm(a);
5056
+ b = this.frac2mm(b);
5057
+ c = this.frac2mm(c);
5058
+ a = this.swizzleVec3MM(a, axCorSag);
5059
+ b = this.swizzleVec3MM(b, axCorSag);
5060
+ c = this.swizzleVec3MM(c, axCorSag);
5061
+ let denom = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
5062
+ let yMult = (b[0] - a[0]) * (c[2] - a[2]) - (c[0] - a[0]) * (b[2] - a[2]);
5063
+ yMult /= denom;
5064
+ let xMult = (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2]);
5065
+ xMult /= denom;
5066
+ let AxyzMxy = [0, 0, 0, 0, 0];
5067
+ AxyzMxy[0] = a[0];
5068
+ AxyzMxy[1] = a[1];
5069
+ AxyzMxy[2] = a[2];
5070
+ AxyzMxy[3] = xMult;
5071
+ AxyzMxy[4] = yMult;
5072
+ return AxyzMxy;
5073
+ };
5074
+
5075
+ Niivue.prototype.draw2DMM = function (
5076
+ leftTopWidthHeight,
5077
+ axCorSag,
5078
+ customMM = NaN
5079
+ ) {
5080
+ let screen = this.screenFieldOfViewExtendedMM(axCorSag);
5081
+ let isRadiolgical =
5082
+ this.opts.isRadiologicalConvention && axCorSag < this.sliceTypeSagittal;
5083
+ if (customMM === Infinity || customMM === -Infinity) {
5084
+ isRadiolgical = customMM !== Infinity;
5085
+ if (axCorSag === this.sliceTypeCoronal) isRadiolgical = !isRadiolgical;
5086
+ } else if (this.opts.sagittalNoseLeft && axCorSag === this.sliceTypeSagittal)
5087
+ isRadiolgical = !isRadiolgical;
5088
+ let elevation = 0;
5089
+ let azimuth = 0;
5090
+ if (axCorSag === this.sliceTypeSagittal) {
5091
+ azimuth = isRadiolgical ? 90 : -90;
5092
+ } else if (axCorSag === this.sliceTypeCoronal) {
5093
+ azimuth = isRadiolgical ? 180 : 0;
5094
+ } else {
5095
+ azimuth = isRadiolgical ? 180 : 0;
5096
+ elevation = isRadiolgical ? -90 : 90;
5097
+ }
5098
+ if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0)
5099
+ //use full screen
5100
+ leftTopWidthHeight = this.scaleSlice(screen.fovMM[0], screen.fovMM[1]);
5101
+ if (isNaN(customMM)) {
5102
+ let panXY = this.swizzleVec3MM(this.scene.pan2Dxyzmm, axCorSag);
5103
+ screen.mnMM[0] -= panXY[0];
5104
+ screen.mxMM[0] -= panXY[0];
5105
+ screen.mnMM[1] -= panXY[1];
5106
+ screen.mxMM[1] -= panXY[1];
5107
+ screen.mnMM[0] /= this.scene.pan2Dxyzmm[3];
5108
+ screen.mxMM[0] /= this.scene.pan2Dxyzmm[3];
5109
+ screen.mnMM[1] /= this.scene.pan2Dxyzmm[3];
5110
+ screen.mxMM[1] /= this.scene.pan2Dxyzmm[3];
5111
+ }
5112
+ let sliceDim = 2; //axial depth is NIfTI k dimension
5113
+ if (axCorSag === this.sliceTypeCoronal) sliceDim = 1; //sagittal depth is NIfTI j dimension
5114
+ if (axCorSag === this.sliceTypeSagittal) sliceDim = 0; //sagittal depth is NIfTI i dimension
5115
+ let sliceFrac = this.scene.crosshairPos[sliceDim];
5116
+ let mm = this.frac2mm(this.scene.crosshairPos);
5117
+ if (!isNaN(customMM) && customMM !== Infinity && customMM !== -Infinity) {
5118
+ mm = this.frac2mm([0.5, 0.5, 0.5]);
5119
+ mm[sliceDim] = customMM;
5120
+ let frac = this.mm2frac(mm);
5121
+ sliceFrac = frac[sliceDim];
4516
5122
  }
4517
- //draw fibers
4518
- if (!hasFibers) {
4519
- gl.enable(gl.BLEND);
4520
- gl.depthFunc(gl.ALWAYS);
5123
+ let sliceMM = mm[sliceDim];
5124
+ let gl = this.gl;
5125
+ gl.clear(gl.DEPTH_BUFFER_BIT);
5126
+ let obj = this.calculateMvpMatrix2D(
5127
+ leftTopWidthHeight,
5128
+ screen.mnMM,
5129
+ screen.mxMM,
5130
+ Infinity,
5131
+ 0,
5132
+ azimuth,
5133
+ elevation,
5134
+ isRadiolgical
5135
+ );
5136
+ if (customMM === Infinity || customMM === -Infinity) {
5137
+ //draw rendering
5138
+ this.draw3D(
5139
+ leftTopWidthHeight,
5140
+ obj.modelViewProjectionMatrix,
5141
+ obj.modelMatrix,
5142
+ obj.normalMatrix,
5143
+ azimuth,
5144
+ elevation
5145
+ );
4521
5146
  return;
4522
5147
  }
4523
- let shader = this.fiberShader;
4524
- shader.use(this.gl);
4525
- gl.uniformMatrix4fv(shader.uniforms["mvpMtx"], false, m);
4526
- gl.uniform1f(shader.uniforms["opacity"], alpha);
4527
- for (let i = 0; i < this.meshes.length; i++) {
4528
- if (this.meshes[i].indexCount < 3) continue;
4529
- if (!this.meshes[i].offsetPt0) continue;
4530
- gl.bindVertexArray(this.meshes[i].vao);
4531
- gl.drawElements(
4532
- gl.LINE_STRIP,
4533
- this.meshes[i].indexCount,
4534
- gl.UNSIGNED_INT,
4535
- 0
5148
+ gl.enable(gl.DEPTH_TEST);
5149
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5150
+ //draw the slice
5151
+ gl.disable(gl.BLEND);
5152
+ gl.depthFunc(gl.GREATER);
5153
+ gl.disable(gl.CULL_FACE); //show front and back faces
5154
+ this.sliceMMShader.use(this.gl);
5155
+ this.gl.uniform1i(
5156
+ this.sliceMMShader.uniforms["backgroundMasksOverlays"],
5157
+ this.backgroundMasksOverlays
5158
+ );
5159
+ this.gl.uniform1f(
5160
+ this.sliceMMShader.uniforms["opacity"],
5161
+ this.volumes[0].opacity
5162
+ );
5163
+ this.gl.uniform1i(this.sliceMMShader.uniforms["axCorSag"], axCorSag);
5164
+ this.gl.uniform1f(this.sliceMMShader.uniforms["slice"], sliceFrac);
5165
+ this.gl.uniformMatrix4fv(
5166
+ this.sliceMMShader.uniforms["frac2mm"],
5167
+ false,
5168
+ this.volumes[0].frac2mm
5169
+ );
5170
+ this.gl.uniformMatrix4fv(
5171
+ this.sliceMMShader.uniforms["mvpMtx"],
5172
+ false,
5173
+ obj.modelViewProjectionMatrix.slice()
5174
+ );
5175
+ this.gl.bindVertexArray(this.genericVAO); //set vertex attributes
5176
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
5177
+ this.gl.bindVertexArray(this.unusedVAO); //set vertex attributes
5178
+ //record screenSlices to detect mouse click positions
5179
+ this.screenSlices.push({
5180
+ leftTopWidthHeight: leftTopWidthHeight,
5181
+ axCorSag: axCorSag,
5182
+ sliceFrac: sliceFrac,
5183
+ AxyzMxy: this.xyMM2xyzMM(axCorSag, sliceFrac),
5184
+ leftTopMM: obj.leftTopMM,
5185
+ fovMM: obj.fovMM,
5186
+ });
5187
+ if (isNaN(customMM)) {
5188
+ //draw crosshairs
5189
+ this.drawCrosshairs3D(true, 1.0, obj.modelViewProjectionMatrix);
5190
+ }
5191
+ if (this.opts.meshThicknessOn2D > 0.0) {
5192
+ if (this.opts.meshThicknessOn2D !== Infinity)
5193
+ obj = this.calculateMvpMatrix2D(
5194
+ leftTopWidthHeight,
5195
+ screen.mnMM,
5196
+ screen.mxMM,
5197
+ this.opts.meshThicknessOn2D,
5198
+ sliceMM,
5199
+ azimuth,
5200
+ elevation,
5201
+ isRadiolgical
5202
+ );
5203
+ this.drawMesh3D(
5204
+ true,
5205
+ 1,
5206
+ obj.modelViewProjectionMatrix,
5207
+ obj.modelMatrix,
5208
+ obj.normalMatrix
4536
5209
  );
4537
- gl.bindVertexArray(this.unusedVAO);
4538
5210
  }
4539
- gl.enable(gl.BLEND);
4540
- gl.depthFunc(gl.ALWAYS);
4541
- this.readyForSync = true;
5211
+
5212
+ if (isNaN(customMM))
5213
+ //no crossbars for mosaic view
5214
+ this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix);
5215
+ this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
4542
5216
  //gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
4543
- }; //drawMesh3D()
5217
+ this.readyForSync = true;
5218
+ }; // draw2DMM()
4544
5219
 
4545
5220
  // not included in public docs
4546
- Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
5221
+ Niivue.prototype.draw2DVox = function (
5222
+ leftTopWidthHeight,
5223
+ axCorSag,
5224
+ customMM = NaN
5225
+ ) {
5226
+ let fovMM = this.screenFieldOfViewMM(axCorSag);
5227
+ if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
5228
+ //use full screen
5229
+ leftTopWidthHeight = this.scaleSlice(fovMM[0], fovMM[1]);
5230
+ }
4547
5231
  this.gl.cullFace(this.gl.FRONT);
5232
+ let ltwh = leftTopWidthHeight.slice();
4548
5233
  let gl = this.gl;
4549
5234
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5235
+ //gl.viewport(ltwh[0], ltwh[1], ltwh[2], ltwh[3]);
4550
5236
  gl.disable(gl.DEPTH_TEST);
4551
- /* issue56
4552
- if (axCorSag === this.sliceTypeAxial) {
4553
- gl.clearDepth(0.0);
4554
- gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
4555
- gl.clear(gl.COLOR_BUFFER_BIT);
4556
-
4557
- this.drawMesh3DX(true,1.0,leftTopWidthHeight);
4558
- this.setPivot3D();
4559
- return;
4560
- }*/
4561
- var crossXYZ = [
5237
+ let sliceDim = 2;
5238
+ let crossXYZ = [
4562
5239
  this.scene.crosshairPos[0],
4563
5240
  this.scene.crosshairPos[1],
4564
5241
  this.scene.crosshairPos[2],
4565
5242
  ]; //axial: width=i, height=j, slice=k
4566
- if (axCorSag === this.sliceTypeCoronal)
5243
+ if (axCorSag === this.sliceTypeCoronal) {
5244
+ sliceDim = 1;
4567
5245
  crossXYZ = [
4568
5246
  this.scene.crosshairPos[0],
4569
5247
  this.scene.crosshairPos[2],
4570
5248
  this.scene.crosshairPos[1],
4571
5249
  ]; //coronal: width=i, height=k, slice=j
4572
- if (axCorSag === this.sliceTypeSagittal)
5250
+ }
5251
+ if (axCorSag === this.sliceTypeSagittal) {
5252
+ sliceDim = 0;
4573
5253
  crossXYZ = [
4574
5254
  this.scene.crosshairPos[1],
4575
5255
  this.scene.crosshairPos[2],
4576
5256
  this.scene.crosshairPos[0],
4577
5257
  ]; //sagittal: width=j, height=k, slice=i
5258
+ }
5259
+ if (!isNaN(customMM)) {
5260
+ let mm = this.frac2mm([0.5, 0.5, 0.5]);
5261
+ mm[sliceDim] = customMM;
5262
+ let frac = this.mm2frac(mm);
5263
+ crossXYZ[2] = frac[sliceDim];
5264
+ }
4578
5265
  let isMirrorLR =
4579
5266
  this.opts.isRadiologicalConvention && axCorSag < this.sliceTypeSagittal;
5267
+ if (this.opts.sagittalNoseLeft && axCorSag === this.sliceTypeSagittal)
5268
+ isMirrorLR = !isMirrorLR;
4580
5269
  this.sliceShader.use(this.gl);
4581
5270
  this.gl.uniform1f(
4582
5271
  this.sliceShader.uniforms["opacity"],
@@ -4604,29 +5293,35 @@ gl.clear(gl.COLOR_BUFFER_BIT);
4604
5293
  leftTopWidthHeight[2],
4605
5294
  leftTopWidthHeight[3]
4606
5295
  );
4607
- //gl.uniform4f(sliceShader.uniforms["leftTopWidthHeight"], leftTopWidthHeight[0], leftTopWidthHeight[1], leftTopWidthHeight[2], leftTopWidthHeight[3]);
4608
- //gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
4609
5296
  this.gl.bindVertexArray(this.genericVAO); //set vertex attributes
4610
5297
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
4611
5298
  //record screenSlices to detect mouse click positions
4612
- this.screenSlices[this.numScreenSlices].leftTopWidthHeight =
4613
- leftTopWidthHeight;
4614
- this.screenSlices[this.numScreenSlices].axCorSag = axCorSag;
4615
- this.numScreenSlices += 1;
5299
+ this.screenSlices.push({
5300
+ leftTopWidthHeight: leftTopWidthHeight,
5301
+ axCorSag: axCorSag,
5302
+ sliceFrac: crossXYZ[2],
5303
+ AxyzMxy: [],
5304
+ leftTopMM: [],
5305
+ fovMM: [fovMM[0], fovMM[1]],
5306
+ });
5307
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5308
+ this.drawSliceOrientationText(ltwh, axCorSag);
5309
+ if (!isNaN(customMM)) return;
4616
5310
  if (this.opts.crosshairWidth <= 0.0) return;
4617
- this.lineShader.use(this.gl);
5311
+ this.gl.bindVertexArray(this.genericVAO);
5312
+ this.rectShader.use(this.gl);
4618
5313
  this.gl.uniform4fv(
4619
- this.lineShader.uniforms["lineColor"],
5314
+ this.rectShader.uniforms["lineColor"],
4620
5315
  this.opts.crosshairColor
4621
5316
  );
4622
- this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
5317
+ this.gl.uniform2fv(this.rectShader.uniforms["canvasWidthHeight"], [
4623
5318
  this.gl.canvas.width,
4624
5319
  this.gl.canvas.height,
4625
5320
  ]);
4626
5321
  //vertical line of crosshair:
4627
5322
  var xleft = leftTopWidthHeight[0] + leftTopWidthHeight[2] * crossXYZ[0];
4628
5323
  this.gl.uniform4f(
4629
- this.lineShader.uniforms["leftTopWidthHeight"],
5324
+ this.rectShader.uniforms["leftTopWidthHeight"],
4630
5325
  xleft - 0.5 * this.opts.crosshairWidth,
4631
5326
  leftTopWidthHeight[1],
4632
5327
  this.opts.crosshairWidth,
@@ -4637,7 +5332,7 @@ gl.clear(gl.COLOR_BUFFER_BIT);
4637
5332
  var xtop =
4638
5333
  leftTopWidthHeight[1] + leftTopWidthHeight[3] * (1.0 - crossXYZ[1]);
4639
5334
  this.gl.uniform4f(
4640
- this.lineShader.uniforms["leftTopWidthHeight"],
5335
+ this.rectShader.uniforms["leftTopWidthHeight"],
4641
5336
  leftTopWidthHeight[0],
4642
5337
  xtop - 0.5 * this.opts.crosshairWidth,
4643
5338
  leftTopWidthHeight[2],
@@ -4645,60 +5340,34 @@ gl.clear(gl.COLOR_BUFFER_BIT);
4645
5340
  );
4646
5341
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
4647
5342
  this.gl.bindVertexArray(this.unusedVAO); //set vertex attributes
4648
-
4649
5343
  this.gl.enable(this.gl.CULL_FACE);
4650
- if (isMirrorLR)
4651
- this.drawTextRight(
4652
- [
4653
- leftTopWidthHeight[0] + leftTopWidthHeight[2] + 1,
4654
- leftTopWidthHeight[1] + 0.5 * leftTopWidthHeight[3],
4655
- ],
4656
- "R"
4657
- );
4658
- else if (axCorSag < this.sliceTypeSagittal)
4659
- this.drawTextRight(
4660
- [
4661
- leftTopWidthHeight[0] + 1,
4662
- leftTopWidthHeight[1] + 0.5 * leftTopWidthHeight[3],
4663
- ],
4664
- "L"
4665
- );
4666
- if (axCorSag === this.sliceTypeAxial)
4667
- this.drawTextBelow(
4668
- [
4669
- leftTopWidthHeight[0] + 0.5 * leftTopWidthHeight[2],
4670
- leftTopWidthHeight[1] + 1,
4671
- ],
4672
- "A"
4673
- );
4674
- if (axCorSag > this.sliceTypeAxial)
4675
- this.drawTextBelow(
4676
- [
4677
- leftTopWidthHeight[0] + 0.5 * leftTopWidthHeight[2],
4678
- leftTopWidthHeight[1] + 1,
4679
- ],
4680
- "S"
4681
- );
4682
-
4683
- if (axCorSag !== this.sliceTypeAxial) return;
4684
- gl.viewport(
4685
- leftTopWidthHeight[0],
4686
- leftTopWidthHeight[1],
4687
- leftTopWidthHeight[2],
4688
- leftTopWidthHeight[3]
4689
- );
4690
5344
  gl.enable(gl.DEPTH_TEST);
4691
5345
  gl.depthFunc(gl.ALWAYS);
4692
5346
  gl.clearDepth(0.0);
4693
-
4694
5347
  this.sync();
4695
- }; // draw2D()
5348
+ }; // draw2DVox()
4696
5349
 
4697
- Niivue.prototype.calculateMvpMatrix = function () {
4698
- function deg2rad(deg) {
4699
- return deg * (Math.PI / 180.0);
4700
- }
4701
- let whratio = this.gl.canvas.clientWidth / this.gl.canvas.clientHeight;
5350
+ Niivue.prototype.draw2D = function (
5351
+ leftTopWidthHeight,
5352
+ axCorSag,
5353
+ customMM = NaN
5354
+ ) {
5355
+ if (this.opts.isSliceMM)
5356
+ this.draw2DMM(leftTopWidthHeight, axCorSag, customMM);
5357
+ else this.draw2DVox(leftTopWidthHeight, axCorSag, customMM);
5358
+ };
5359
+
5360
+ Niivue.prototype.calculateMvpMatrix = function (
5361
+ unused,
5362
+ leftTopWidthHeight = [0, 0, 0, 0],
5363
+ azimuth,
5364
+ elevation
5365
+ ) {
5366
+ if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0)
5367
+ //use full canvas
5368
+ leftTopWidthHeight = [0, 0, this.gl.canvas.width, this.gl.canvas.height];
5369
+ let whratio = leftTopWidthHeight[2] / leftTopWidthHeight[3];
5370
+ //let whratio = this.gl.canvas.clientWidth / this.gl.canvas.clientHeight;
4702
5371
  //pivot from center of objects
4703
5372
  //let scale = this.furthestVertexFromOrigin;
4704
5373
  //let origin = [0,0,0];
@@ -4737,26 +5406,9 @@ Niivue.prototype.calculateMvpMatrix = function () {
4737
5406
  if (this.position)
4738
5407
  mat.mat4.translate(modelMatrix, modelMatrix, this.position);
4739
5408
  //apply elevation
4740
- mat.mat4.rotateX(
4741
- modelMatrix,
4742
- modelMatrix,
4743
- deg2rad(270 - this.scene.renderElevation)
4744
- );
5409
+ mat.mat4.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
4745
5410
  //apply azimuth
4746
- mat.mat4.rotateZ(
4747
- modelMatrix,
4748
- modelMatrix,
4749
- deg2rad(this.scene.renderAzimuth - 180)
4750
- );
4751
- //translate object to be in center of field of view (e.g. CT brain scans where origin is distant table center)
4752
- /*if (this.volumeObject3D) {
4753
- mat.mat4.translate(
4754
- modelMatrix,
4755
- modelMatrix,
4756
- this.volumeObject3D.originNegate
4757
- );
4758
- }*/
4759
-
5411
+ mat.mat4.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
4760
5412
  mat.mat4.translate(modelMatrix, modelMatrix, [
4761
5413
  -origin[0],
4762
5414
  -origin[1],
@@ -4778,7 +5430,7 @@ Niivue.prototype.calculateMvpMatrix = function () {
4778
5430
  }; // calculateMvpMatrix
4779
5431
 
4780
5432
  // not included in public docs
4781
- Niivue.prototype.calculateRayDirection = function () {
5433
+ Niivue.prototype.calculateRayDirection = function (azimuth, elevation) {
4782
5434
  function deg2rad(deg) {
4783
5435
  return deg * (Math.PI / 180.0);
4784
5436
  }
@@ -4786,17 +5438,9 @@ Niivue.prototype.calculateRayDirection = function () {
4786
5438
  modelMatrix[0] = -1; //mirror X coordinate
4787
5439
  //push the model away from the camera so camera not inside model
4788
5440
  //apply elevation
4789
- mat.mat4.rotateX(
4790
- modelMatrix,
4791
- modelMatrix,
4792
- deg2rad(270 - this.scene.renderElevation)
4793
- );
5441
+ mat.mat4.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
4794
5442
  //apply azimuth
4795
- mat.mat4.rotateZ(
4796
- modelMatrix,
4797
- modelMatrix,
4798
- deg2rad(this.scene.renderAzimuth - 180)
4799
- );
5443
+ mat.mat4.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
4800
5444
  if (this.back.obliqueRAS) {
4801
5445
  let oblique = mat.mat4.clone(this.back.obliqueRAS);
4802
5446
  mat.mat4.multiply(modelMatrix, modelMatrix, oblique);
@@ -4883,6 +5527,8 @@ Niivue.prototype.setPivot3D = function () {
4883
5527
  this.pivot3D = [pivot[0], pivot[1], pivot[2]];
4884
5528
  //find scale of scene
4885
5529
  mat.vec3.subtract(pivot, mx, mn);
5530
+ this.extentsMin = mn;
5531
+ this.extentsMax = mx;
4886
5532
  this.furthestFromPivot = mat.vec3.length(pivot) * 0.5; //pivot is half way between the extreme vertices
4887
5533
  }; // setPivot3D()
4888
5534
 
@@ -4911,14 +5557,14 @@ Niivue.prototype.drawGraphLine = function (
4911
5557
  };
4912
5558
 
4913
5559
  Niivue.prototype.drawRect = function (LTWH, color = [1, 0, 0, 0.5]) {
4914
- this.lineShader.use(this.gl);
4915
- this.gl.uniform4fv(this.lineShader.uniforms["lineColor"], color);
4916
- this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
5560
+ this.rectShader.use(this.gl);
5561
+ this.gl.uniform4fv(this.rectShader.uniforms["lineColor"], color);
5562
+ this.gl.uniform2fv(this.rectShader.uniforms["canvasWidthHeight"], [
4917
5563
  this.gl.canvas.width,
4918
5564
  this.gl.canvas.height,
4919
5565
  ]);
4920
5566
  this.gl.uniform4f(
4921
- this.lineShader.uniforms["leftTopWidthHeight"],
5567
+ this.rectShader.uniforms["leftTopWidthHeight"],
4922
5568
  LTWH[0],
4923
5569
  LTWH[1],
4924
5570
  LTWH[2],
@@ -4929,21 +5575,6 @@ Niivue.prototype.drawRect = function (LTWH, color = [1, 0, 0, 0.5]) {
4929
5575
  this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
4930
5576
  };
4931
5577
 
4932
- function tickSpacing(tickCount, mn, mx) {
4933
- //https://www.realtimerendering.com/resources/GraphicsGems/gems/Label.c
4934
- //https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
4935
- let range = Math.abs(mx - mn);
4936
- if (range === 0.0) return [0, 0];
4937
- let unroundedTickSize = range / (tickCount - 1);
4938
- let x = Math.ceil(Math.log10(unroundedTickSize) - 1);
4939
- let pow10x = Math.pow(10, x);
4940
- let spacing = Math.ceil(unroundedTickSize / pow10x) * pow10x;
4941
- let ticMin = mn;
4942
- if ((mn / spacing) % 1 !== 0.0)
4943
- ticMin = Math.sign(ticMin) * Math.round(Math.abs(ticMin));
4944
- return [spacing, ticMin];
4945
- }
4946
-
4947
5578
  Niivue.prototype.drawGraph = function () {
4948
5579
  let gl = this.gl;
4949
5580
  let graph = this.graph;
@@ -4951,7 +5582,7 @@ Niivue.prototype.drawGraph = function () {
4951
5582
  this.graph.autoSizeMultiplanar &&
4952
5583
  this.sliceType === this.sliceTypeMultiplanar
4953
5584
  ) {
4954
- for (let i = 0; i < this.numScreenSlices; i++) {
5585
+ for (let i = 0; i < this.screenSlices.length; i++) {
4955
5586
  var axCorSag = this.screenSlices[i].axCorSag;
4956
5587
  if (axCorSag !== this.sliceTypeSagittal) continue;
4957
5588
  var ltwh = this.screenSlices[i].leftTopWidthHeight;
@@ -5041,7 +5672,7 @@ Niivue.prototype.drawGraph = function () {
5041
5672
  graph.backColor[3],
5042
5673
  ];
5043
5674
  this.drawRect(graph.LTWH, borderColor);
5044
- let [spacing, ticMin] = tickSpacing(5, mn, mx);
5675
+ let [spacing, ticMin] = tickSpacing(mn, mx);
5045
5676
  //determine font size
5046
5677
  function humanize(x) {
5047
5678
  //drop trailing zeros from numerical string
@@ -5186,27 +5817,78 @@ Niivue.prototype.drawGraph = function () {
5186
5817
  }
5187
5818
  };
5188
5819
 
5820
+ function isRadiological(mtx) {
5821
+ let vRight = mat.vec4.fromValues(1, 0, 0, 0); //pure right vector
5822
+ let vRotated = mat.vec4.create();
5823
+ mat.vec4.transformMat4(vRotated, vRight, mtx);
5824
+ return vRotated[0];
5825
+ } //n.b. ambiguous for pure sagittal views
5826
+
5189
5827
  // not included in public docs
5190
- Niivue.prototype.draw3D = function () {
5828
+ Niivue.prototype.draw3D = function (
5829
+ leftTopWidthHeight = [0, 0, 0, 0],
5830
+ mvpMatrix = null,
5831
+ modelMatrix,
5832
+ normalMatrix,
5833
+ azimuth = null,
5834
+ elevation
5835
+ ) {
5836
+ let isMosaic = azimuth !== null;
5191
5837
  this.setPivot3D();
5838
+ if (!isMosaic) {
5839
+ azimuth = this.scene.renderAzimuth;
5840
+ elevation = this.scene.renderElevation;
5841
+ }
5192
5842
  let gl = this.gl;
5193
- gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5843
+ if (mvpMatrix === null)
5844
+ [mvpMatrix, modelMatrix, normalMatrix] = this.calculateMvpMatrix(
5845
+ null,
5846
+ leftTopWidthHeight,
5847
+ azimuth,
5848
+ elevation
5849
+ );
5850
+ if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
5851
+ //use full canvas
5852
+ leftTopWidthHeight = [0, 0, gl.canvas.width, gl.canvas.height];
5853
+ this.screenSlices.push({
5854
+ leftTopWidthHeight: leftTopWidthHeight,
5855
+ axCorSag: this.sliceTypeRender,
5856
+ sliceFrac: 0,
5857
+ AxyzMxy: [],
5858
+ leftTopMM: [],
5859
+ fovMM: [isRadiological(modelMatrix), 0],
5860
+ });
5861
+ } else {
5862
+ this.screenSlices.push({
5863
+ leftTopWidthHeight: leftTopWidthHeight.slice(),
5864
+ axCorSag: this.sliceTypeRender,
5865
+ sliceFrac: 0,
5866
+ AxyzMxy: [],
5867
+ leftTopMM: [],
5868
+ fovMM: [isRadiological(modelMatrix), 0],
5869
+ });
5870
+ leftTopWidthHeight[1] =
5871
+ gl.canvas.height - leftTopWidthHeight[3] - leftTopWidthHeight[1];
5872
+ }
5873
+ gl.viewport(
5874
+ leftTopWidthHeight[0],
5875
+ leftTopWidthHeight[1],
5876
+ leftTopWidthHeight[2],
5877
+ leftTopWidthHeight[3]
5878
+ );
5194
5879
  gl.enable(gl.DEPTH_TEST);
5195
5880
  gl.depthFunc(gl.ALWAYS);
5196
5881
  gl.clearDepth(0.0);
5882
+
5197
5883
  if (this.volumes.length === 0) {
5198
5884
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
5199
- this.drawMesh3D(true, 1);
5885
+ this.drawMesh3D(true, 1, mvpMatrix, modelMatrix, normalMatrix);
5886
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5200
5887
  return;
5201
5888
  }
5202
-
5203
5889
  // mvp matrix and ray direction can now be a constant because of world space
5204
- let mvpMatrix, modelMatrix, normalMatrix;
5205
- // eslint-disable-next-line no-unused-vars
5206
- [mvpMatrix, modelMatrix, normalMatrix] = this.calculateMvpMatrix(
5207
- this.volumeObject3D
5208
- );
5209
- const rayDir = this.calculateRayDirection();
5890
+ const rayDir = this.calculateRayDirection(azimuth, elevation);
5891
+
5210
5892
  let object3D = this.volumeObject3D;
5211
5893
  // render picking surfaces
5212
5894
  if (this.scene.mouseDepthPicker) {
@@ -5227,10 +5909,11 @@ Niivue.prototype.draw3D = function () {
5227
5909
  gl.bindVertexArray(this.unusedVAO);
5228
5910
  }
5229
5911
  //check if we have a selected object
5230
- const pixelX = (this.mousePos[0] * gl.canvas.width) / gl.canvas.clientWidth;
5912
+ const pixelX =
5913
+ (this.mousePos[0] * leftTopWidthHeight[2]) / leftTopWidthHeight[2];
5231
5914
  const pixelY =
5232
5915
  gl.canvas.height -
5233
- (this.mousePos[1] * gl.canvas.height) / gl.canvas.clientHeight -
5916
+ (this.mousePos[1] * leftTopWidthHeight[3]) / leftTopWidthHeight[3] -
5234
5917
  1;
5235
5918
  const rgbaPixel = new Uint8Array(4);
5236
5919
  gl.readPixels(
@@ -5272,11 +5955,12 @@ Niivue.prototype.draw3D = function () {
5272
5955
  gl.drawElements(object3D.mode, object3D.indexCount, gl.UNSIGNED_SHORT, 0);
5273
5956
  gl.bindVertexArray(this.unusedVAO);
5274
5957
  }
5275
- this.drawCrosshairs3D(true, 1.0);
5276
- this.drawMesh3D(true, 1.0);
5277
- this.drawMesh3D(false, 0.02);
5958
+ if (!isMosaic) this.drawCrosshairs3D(true, 1.0, mvpMatrix);
5959
+ this.drawMesh3D(true, 1.0, mvpMatrix, modelMatrix, normalMatrix);
5960
+ this.drawMesh3D(false, 0.02, mvpMatrix, modelMatrix, normalMatrix);
5278
5961
  // this.drawMesh3D(true, 1.0);
5279
- this.drawCrosshairs3D(false, 0.15);
5962
+ if (!isMosaic) this.drawCrosshairs3D(false, 0.15, mvpMatrix);
5963
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5280
5964
  let posString =
5281
5965
  "azimuth: " +
5282
5966
  this.scene.renderAzimuth.toFixed(0) +
@@ -5289,12 +5973,22 @@ Niivue.prototype.draw3D = function () {
5289
5973
  return posString;
5290
5974
  }; // draw3D()
5291
5975
 
5292
- Niivue.prototype.drawMesh3D = function (isDepthTest = true, alpha = 1.0) {
5976
+ Niivue.prototype.drawMesh3D = function (
5977
+ isDepthTest = true,
5978
+ alpha = 1.0,
5979
+ m,
5980
+ modelMtx,
5981
+ normMtx
5982
+ ) {
5293
5983
  if (this.meshes.length < 1) return;
5294
5984
  let gl = this.gl;
5295
- let m, modelMtx, normMtx;
5296
- [m, modelMtx, normMtx] = this.calculateMvpMatrix(this.volumeObject3D);
5297
-
5985
+ //let m, modelMtx, normMtx;
5986
+ if (!m)
5987
+ [m, modelMtx, normMtx] = this.calculateMvpMatrix(
5988
+ this.volumeObject3D,
5989
+ this.scene.renderAzimuth,
5990
+ this.scene.renderElevation
5991
+ );
5298
5992
  gl.enable(gl.DEPTH_TEST);
5299
5993
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5300
5994
 
@@ -5360,7 +6054,11 @@ Niivue.prototype.drawMesh3D = function (isDepthTest = true, alpha = 1.0) {
5360
6054
  this.readyForSync = true;
5361
6055
  }; //drawMesh3D()
5362
6056
 
5363
- Niivue.prototype.drawCrosshairs3D = function (isDepthTest = true, alpha = 1.0) {
6057
+ Niivue.prototype.drawCrosshairs3D = function (
6058
+ isDepthTest = true,
6059
+ alpha = 1.0,
6060
+ mvpMtx = null
6061
+ ) {
5364
6062
  if (!this.opts.show3Dcrosshair) return;
5365
6063
  let gl = this.gl;
5366
6064
  let mm = this.frac2mm(this.scene.crosshairPos);
@@ -5376,10 +6074,12 @@ Niivue.prototype.drawCrosshairs3D = function (isDepthTest = true, alpha = 1.0) {
5376
6074
  gl.deleteBuffer(this.crosshairs3D.indexBuffer); //TODO: handle in nvimage.js: create once, update with bufferSubData
5377
6075
  gl.deleteBuffer(this.crosshairs3D.vertexBuffer); //TODO: handle in nvimage.js: create once, update with bufferSubData
5378
6076
  }
5379
- let radius = Math.min(
5380
- Math.min(this.back.pixDims[1], this.back.pixDims[2]),
5381
- this.back.pixDims[3]
5382
- );
6077
+ let radius =
6078
+ 0.5 *
6079
+ Math.min(
6080
+ Math.min(this.back.pixDims[1], this.back.pixDims[2]),
6081
+ this.back.pixDims[3]
6082
+ );
5383
6083
  this.crosshairs3D = NiivueObject3D.generateCrosshairs(
5384
6084
  this.gl,
5385
6085
  1,
@@ -5395,13 +6095,17 @@ Niivue.prototype.drawCrosshairs3D = function (isDepthTest = true, alpha = 1.0) {
5395
6095
  }
5396
6096
  let crosshairsShader = this.surfaceShader;
5397
6097
  crosshairsShader.use(this.gl);
5398
- let m, modelMtx, normMtx;
6098
+ let modelMtx, normMtx;
5399
6099
  // eslint-disable-next-line no-unused-vars
5400
- [m, modelMtx, normMtx] = this.calculateMvpMatrix(this.crosshairs3D);
5401
- gl.uniformMatrix4fv(crosshairsShader.uniforms["mvpMtx"], false, m);
6100
+ if (mvpMtx == null)
6101
+ [mvpMtx, modelMtx, normMtx] = this.calculateMvpMatrix(
6102
+ this.crosshairs3D,
6103
+ this.scene.renderAzimuth,
6104
+ this.scene.renderElevation
6105
+ );
6106
+ gl.uniformMatrix4fv(crosshairsShader.uniforms["mvpMtx"], false, mvpMtx);
5402
6107
 
5403
6108
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.crosshairs3D.indexBuffer);
5404
-
5405
6109
  gl.enable(gl.DEPTH_TEST);
5406
6110
  let color = [...this.opts.crosshairColor];
5407
6111
  if (isDepthTest) {
@@ -5413,6 +6117,7 @@ Niivue.prototype.drawCrosshairs3D = function (isDepthTest = true, alpha = 1.0) {
5413
6117
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5414
6118
  gl.depthFunc(gl.ALWAYS);
5415
6119
  }
6120
+
5416
6121
  color[3] = alpha;
5417
6122
  gl.uniform4fv(crosshairsShader.uniforms["surfaceColor"], color);
5418
6123
  gl.bindVertexArray(this.crosshairs3D.vao);
@@ -5480,94 +6185,92 @@ Niivue.prototype.moveCrosshairInVox = function (x, y, z) {
5480
6185
  this.drawScene();
5481
6186
  };
5482
6187
 
5483
- // not included in public docs
5484
6188
  Niivue.prototype.frac2mm = function (frac, volIdx = 0) {
5485
- //convert from normalized texture space XYZ= [0..1, 0..1 ,0..1] to object space in millimeters
5486
6189
  let pos = mat.vec4.fromValues(frac[0], frac[1], frac[2], 1);
5487
- //let d = overlayItem.hdr.dims;
5488
- let dim = mat.vec4.fromValues(
5489
- this.volumes[volIdx].dimsRAS[1],
5490
- this.volumes[volIdx].dimsRAS[2],
5491
- this.volumes[volIdx].dimsRAS[3],
5492
- 1
5493
- );
5494
- let sform = mat.mat4.clone(this.volumes[volIdx].matRAS);
5495
- mat.mat4.transpose(sform, sform);
5496
- mat.vec4.mul(pos, pos, dim);
5497
- let shim = mat.vec4.fromValues(-0.5, -0.5, -0.5, 0); //bitmap with 5 voxels scaled 0..1, voxel centers are 0.1,0.3,0.5,0.7,0.9
5498
- mat.vec4.add(pos, pos, shim);
5499
- mat.vec4.transformMat4(pos, pos, sform);
6190
+ mat.vec4.transformMat4(pos, pos, this.volumes[volIdx].frac2mm);
5500
6191
  return pos;
5501
- }; // frac2mm()
5502
-
5503
- // not included in public docs
5504
- Niivue.prototype.canvasPos2frac = function (canvasPos) {
5505
- let x, y, z;
5506
-
5507
- // convert canvas pos to normalized texture space
5508
- for (let i = 0; i < this.numScreenSlices; i++) {
5509
- var axCorSag = this.screenSlices[i].axCorSag;
5510
- if (axCorSag > this.sliceTypeSagittal) continue;
6192
+ };
5511
6193
 
5512
- var ltwh = this.screenSlices[i].leftTopWidthHeight;
5513
- let isMirror = false;
5514
- if (ltwh[2] < 0) {
5515
- isMirror = true;
5516
- ltwh[0] += ltwh[2];
5517
- ltwh[2] = -ltwh[2];
6194
+ Niivue.prototype.screenXY2TextureFrac = function (x, y, i) {
6195
+ let texFrac = [-1, -1, -1]; //texture 0..1 so -1 is out of bounds
6196
+ var axCorSag = this.screenSlices[i].axCorSag;
6197
+ if (axCorSag > this.sliceTypeSagittal) return texFrac;
6198
+ var ltwh = this.screenSlices[i].leftTopWidthHeight.slice();
6199
+ let isMirror = false;
6200
+ if (ltwh[2] < 0) {
6201
+ isMirror = true;
6202
+ ltwh[0] += ltwh[2];
6203
+ ltwh[2] = -ltwh[2];
6204
+ }
6205
+ var fracX = (x - ltwh[0]) / ltwh[2];
6206
+ if (isMirror) fracX = 1.0 - fracX;
6207
+ var fracY = 1.0 - (y - ltwh[1]) / ltwh[3];
6208
+ if (fracX < 0.0 || fracX > 1.0 || fracY < 0.0 || fracY > 1.0) return texFrac;
6209
+ if (this.screenSlices[i].AxyzMxy.length > 4) {
6210
+ let xyzMM = [0, 0, 0];
6211
+ xyzMM[0] =
6212
+ this.screenSlices[i].leftTopMM[0] + fracX * this.screenSlices[i].fovMM[0];
6213
+ xyzMM[1] =
6214
+ this.screenSlices[i].leftTopMM[1] + fracY * this.screenSlices[i].fovMM[1];
6215
+ // let xyz = mat.vec3.fromValues(30, 30, 0);
6216
+ let v = this.screenSlices[i].AxyzMxy;
6217
+ xyzMM[2] = v[2] + v[4] * (xyzMM[1] - v[1]) - v[3] * (xyzMM[0] - v[0]);
6218
+ if (axCorSag === this.sliceTypeCoronal)
6219
+ xyzMM = swizzleVec3(xyzMM, [0, 2, 1]); //screen RSA to NIfTI RAS
6220
+ if (axCorSag === this.sliceTypeSagittal)
6221
+ xyzMM = swizzleVec3(xyzMM, [2, 0, 1]); //screen ASR to NIfTI RAS
6222
+ let xyz = this.mm2frac(xyzMM);
6223
+ if (
6224
+ xyz[0] < 0 ||
6225
+ xyz[0] > 1 ||
6226
+ xyz[1] < 0 ||
6227
+ xyz[1] > 1 ||
6228
+ xyz[2] < 0 ||
6229
+ xyz[2] > 1
6230
+ )
6231
+ return texFrac;
6232
+ texFrac[0] = xyz[0];
6233
+ texFrac[1] = xyz[1];
6234
+ texFrac[2] = xyz[2];
6235
+ } else {
6236
+ texFrac = this.scene.crosshairPos.slice();
6237
+ if (axCorSag === this.sliceTypeAxial) {
6238
+ texFrac[0] = fracX;
6239
+ texFrac[1] = fracY;
5518
6240
  }
5519
-
5520
- var fracX = (canvasPos[0] - ltwh[0]) / ltwh[2];
5521
- if (isMirror) {
5522
- fracX = 1.0 - fracX;
6241
+ if (axCorSag === this.sliceTypeCoronal) {
6242
+ texFrac[0] = fracX;
6243
+ texFrac[2] = fracY;
5523
6244
  }
5524
-
5525
- var fracY = 1.0 - (canvasPos[1] - ltwh[1]) / ltwh[3];
5526
- if (fracX >= 0.0 && fracX < 1.0 && fracY >= 0.0 && fracY < 1.0) {
5527
- // this is the slice the user right clicked in
5528
- switch (axCorSag) {
5529
- case this.sliceTypeAxial:
5530
- break;
5531
- case this.sliceTypeCoronal:
5532
- break;
5533
- default:
5534
- }
5535
-
5536
- if (axCorSag === this.sliceTypeAxial) {
5537
- x = fracX;
5538
- y = fracY;
5539
- z = this.scene.crosshairPos[2];
5540
- }
5541
- if (axCorSag === this.sliceTypeCoronal) {
5542
- x = fracX;
5543
- y = this.scene.crosshairPos[1];
5544
- z = fracY;
5545
- }
5546
- if (axCorSag === this.sliceTypeSagittal) {
5547
- x = this.scene.crosshairPos[0];
5548
- y = fracX;
5549
- z = fracY;
5550
- }
5551
-
5552
- break;
6245
+ if (axCorSag === this.sliceTypeSagittal) {
6246
+ texFrac[1] = fracX;
6247
+ texFrac[2] = fracY;
5553
6248
  }
5554
6249
  }
6250
+ return texFrac;
6251
+ }; // screenXY2TextureFrac()
5555
6252
 
5556
- return [x, y, z];
5557
- }; // canvas2frac
6253
+ // not included in public docs
6254
+ Niivue.prototype.canvasPos2frac = function (canvasPos) {
6255
+ for (let i = 0; i < this.screenSlices.length; i++) {
6256
+ let texFrac = this.screenXY2TextureFrac(canvasPos[0], canvasPos[1], i);
6257
+ if (texFrac[0] >= 0) return texFrac;
6258
+ }
6259
+ return [-1, -1, -1]; //texture 0..1 so -1 is out of bounds
6260
+ };
5558
6261
 
5559
6262
  // not included in public docs
5560
6263
  // note: we also have a "sliceScale" method, which could be confusing
5561
6264
  Niivue.prototype.scaleSlice = function (w, h) {
5562
6265
  let scalePix = this.gl.canvas.clientWidth / w;
5563
- if (h * scalePix > this.gl.canvas.clientHeight)
5564
- scalePix = this.gl.canvas.clientHeight / h;
6266
+ if (h * scalePix > this.effectiveCanvasHeight())
6267
+ scalePix = this.effectiveCanvasHeight() / h;
5565
6268
  //canvas space is 0,0...w,h with origin at upper left
5566
6269
  let wPix = w * scalePix;
5567
6270
  let hPix = h * scalePix;
5568
6271
  let leftTopWidthHeight = [
5569
6272
  (this.gl.canvas.clientWidth - wPix) * 0.5,
5570
- (this.gl.canvas.clientHeight - hPix) * 0.5,
6273
+ (this.effectiveCanvasHeight() - hPix) * 0.5,
5571
6274
  wPix,
5572
6275
  hPix,
5573
6276
  ];
@@ -5595,11 +6298,365 @@ Niivue.prototype.drawThumbnail = function () {
5595
6298
  this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
5596
6299
  };
5597
6300
 
6301
+ Niivue.prototype.drawLine = function (leftTopWidthHeight) {
6302
+ this.gl.enable(this.gl.BLEND);
6303
+ this.rectShader.use(this.gl);
6304
+ this.gl.uniform4fv(
6305
+ this.rectShader.uniforms["lineColor"],
6306
+ this.opts.crosshairColor
6307
+ );
6308
+ this.gl.uniform2fv(this.rectShader.uniforms["canvasWidthHeight"], [
6309
+ this.gl.canvas.width,
6310
+ this.gl.canvas.height,
6311
+ ]);
6312
+ //vertical line of crosshair:
6313
+ this.gl.uniform4f(
6314
+ this.rectShader.uniforms["leftTopWidthHeight"],
6315
+ leftTopWidthHeight[0],
6316
+ leftTopWidthHeight[1],
6317
+ leftTopWidthHeight[2],
6318
+ leftTopWidthHeight[3]
6319
+ );
6320
+ this.gl.bindVertexArray(this.genericVAO);
6321
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
6322
+ this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
6323
+ };
6324
+
6325
+ Niivue.prototype.drawLine2DX = function (startXYendXY, thickness = 1) {
6326
+ this.gl.bindVertexArray(this.genericVAO);
6327
+ this.lineShader.use(this.gl);
6328
+ this.gl.uniform4fv(
6329
+ this.lineShader.uniforms["lineColor"],
6330
+ this.opts.crosshairColor
6331
+ );
6332
+ this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
6333
+ this.gl.canvas.width,
6334
+ this.gl.canvas.height,
6335
+ ]);
6336
+ //draw Line
6337
+ this.gl.uniform1f(this.lineShader.uniforms["thickness"], thickness);
6338
+ this.gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], startXYendXY);
6339
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
6340
+ this.gl.bindVertexArray(this.unusedVAO); //set vertex attributes
6341
+ };
6342
+
6343
+ Niivue.prototype.drawCrossLinesMM = function (
6344
+ sliceIndex,
6345
+ axCorSag,
6346
+ axiMM,
6347
+ corMM,
6348
+ sagMM
6349
+ ) {
6350
+ if (sliceIndex < 0 || this.screenSlices.length <= sliceIndex) return;
6351
+ let tile = this.screenSlices[sliceIndex];
6352
+ let linesH = corMM.slice();
6353
+ let linesV = sagMM.slice();
6354
+ let thick = Math.max(1, this.opts.crosshairWidth);
6355
+ if (axCorSag === this.sliceTypeCoronal) linesH = axiMM.slice();
6356
+ if (axCorSag === this.sliceTypeSagittal) {
6357
+ linesH = axiMM.slice();
6358
+ linesV = corMM.slice();
6359
+ }
6360
+ function mm2screen(mm) {
6361
+ let screenXY = [0, 0];
6362
+ screenXY[0] =
6363
+ tile.leftTopWidthHeight[0] +
6364
+ ((mm[0] - tile.leftTopMM[0]) / tile.fovMM[0]) *
6365
+ tile.leftTopWidthHeight[2];
6366
+ screenXY[1] =
6367
+ tile.leftTopWidthHeight[1] +
6368
+ tile.leftTopWidthHeight[3] -
6369
+ ((mm[1] - tile.leftTopMM[1]) / tile.fovMM[1]) *
6370
+ tile.leftTopWidthHeight[3];
6371
+ return screenXY;
6372
+ }
6373
+ if (linesH.length > 0 && axCorSag === 0) {
6374
+ let fracZ = tile.sliceFrac;
6375
+ let dimV = 1;
6376
+ for (let i = 0; i < linesH.length; i++) {
6377
+ let mmV = this.frac2mm([0.5, 0.5, 0.5]);
6378
+ mmV[dimV] = linesH[i];
6379
+ let fracY = this.mm2frac(mmV);
6380
+ fracY = fracY[dimV];
6381
+ let left = this.frac2mm([0.0, fracY, fracZ]);
6382
+ left = swizzleVec3(left, [0, 1, 2]);
6383
+ let right = this.frac2mm([1.0, fracY, fracZ]);
6384
+ right = swizzleVec3(right, [0, 1, 2]);
6385
+ left = mm2screen(left);
6386
+ right = mm2screen(right);
6387
+ this.drawLine2DX([left[0], left[1], right[0], right[1]], thick);
6388
+ }
6389
+ }
6390
+ if (linesH.length > 0 && axCorSag === 1) {
6391
+ let fracH = tile.sliceFrac;
6392
+ let mm = this.frac2mm([0.5, fracH, 0.5]);
6393
+ let dimV = 2;
6394
+ for (let i = 0; i < linesH.length; i++) {
6395
+ let mmV = this.frac2mm([0.5, 0.5, 0.5]);
6396
+ mmV[dimV] = linesH[i];
6397
+ let fracV = this.mm2frac(mmV);
6398
+ fracV = fracV[dimV];
6399
+ let left = this.frac2mm([0.0, fracH, fracV]);
6400
+ left = swizzleVec3(left, [0, 2, 1]);
6401
+ let right = this.frac2mm([1.0, fracH, fracV]);
6402
+ right = swizzleVec3(right, [0, 2, 1]);
6403
+ left = mm2screen(left);
6404
+ right = mm2screen(right);
6405
+ this.drawLine2DX([left[0], left[1], right[0], right[1]], thick);
6406
+ }
6407
+ }
6408
+ if (linesH.length > 0 && axCorSag === 2) {
6409
+ let fracX = tile.sliceFrac;
6410
+ let dimV = 2;
6411
+ for (let i = 0; i < linesH.length; i++) {
6412
+ let mmV = this.frac2mm([0.5, 0.5, 0.5]);
6413
+ mmV[dimV] = linesH[i];
6414
+ let fracZ = this.mm2frac(mmV);
6415
+ fracZ = fracZ[dimV];
6416
+ let left = this.frac2mm([fracX, 0, fracZ]);
6417
+ left = swizzleVec3(left, [1, 2, 0]);
6418
+ let right = this.frac2mm([fracX, 1, fracZ]);
6419
+ right = swizzleVec3(right, [1, 2, 0]);
6420
+ left = mm2screen(left);
6421
+ right = mm2screen(right);
6422
+ this.drawLine2DX([left[0], left[1], right[0], right[1]], thick);
6423
+ }
6424
+ }
6425
+ if (linesV.length > 0 && axCorSag === 0) {
6426
+ let fracZ = tile.sliceFrac;
6427
+ let dimH = 0;
6428
+ for (let i = 0; i < linesV.length; i++) {
6429
+ let mm = this.frac2mm([0.5, 0.5, 0.5]);
6430
+ mm[dimH] = linesV[i];
6431
+ let frac = this.mm2frac(mm);
6432
+ frac = frac[dimH];
6433
+ let left = this.frac2mm([frac, 0, fracZ]);
6434
+ left = swizzleVec3(left, [0, 1, 2]);
6435
+ let right = this.frac2mm([frac, 1, fracZ]);
6436
+ right = swizzleVec3(right, [0, 1, 2]);
6437
+ left = mm2screen(left);
6438
+ right = mm2screen(right);
6439
+ this.drawLine2DX([left[0], left[1], right[0], right[1]], thick);
6440
+ }
6441
+ } //if vertical lines
6442
+ if (linesV.length > 0 && axCorSag === 1) {
6443
+ let fracY = tile.sliceFrac;
6444
+ let dimH = 0;
6445
+ for (let i = 0; i < linesV.length; i++) {
6446
+ let mm = this.frac2mm([0.5, 0.5, 0.5]);
6447
+ mm[dimH] = linesV[i];
6448
+ let frac = this.mm2frac(mm);
6449
+ frac = frac[dimH];
6450
+ let left = this.frac2mm([frac, fracY, 0]);
6451
+ left = swizzleVec3(left, [0, 2, 1]);
6452
+ let right = this.frac2mm([frac, fracY, 1]);
6453
+ right = swizzleVec3(right, [0, 2, 1]);
6454
+ left = mm2screen(left);
6455
+ right = mm2screen(right);
6456
+ this.drawLine2DX([left[0], left[1], right[0], right[1]], thick);
6457
+ }
6458
+ } //if vertical lines
6459
+ if (linesV.length > 0 && axCorSag === 2) {
6460
+ let fracX = tile.sliceFrac;
6461
+ let dimH = 1;
6462
+ for (let i = 0; i < linesV.length; i++) {
6463
+ let mm = this.frac2mm([0.5, 0.5, 0.5]);
6464
+ mm[dimH] = linesV[i];
6465
+ let frac = this.mm2frac(mm);
6466
+ frac = frac[dimH];
6467
+ let left = this.frac2mm([fracX, frac, 0]);
6468
+ left = swizzleVec3(left, [1, 2, 0]);
6469
+ let right = this.frac2mm([fracX, frac, 1]);
6470
+ right = swizzleVec3(right, [1, 2, 0]);
6471
+ left = mm2screen(left);
6472
+ right = mm2screen(right);
6473
+ this.drawLine2DX([left[0], left[1], right[0], right[1]], thick);
6474
+ }
6475
+ } //if vertical lines
6476
+ };
6477
+
6478
+ Niivue.prototype.drawCrossLines = function (
6479
+ sliceIndex,
6480
+ axCorSag,
6481
+ axiMM,
6482
+ corMM,
6483
+ sagMM
6484
+ ) {
6485
+ if (sliceIndex < 0 || this.screenSlices.length <= sliceIndex) return;
6486
+ if (this.opts.isSliceMM)
6487
+ return this.drawCrossLinesMM(sliceIndex, axCorSag, axiMM, corMM, sagMM);
6488
+ let tile = this.screenSlices[sliceIndex];
6489
+ let linesH = corMM.slice();
6490
+ let linesV = sagMM.slice();
6491
+ let thick = Math.max(1, this.opts.crosshairWidth);
6492
+ if (axCorSag === this.sliceTypeCoronal) linesH = axiMM.slice();
6493
+ if (axCorSag === this.sliceTypeSagittal) {
6494
+ linesH = axiMM.slice();
6495
+ linesV = corMM.slice();
6496
+ }
6497
+ if (linesH.length > 0) {
6498
+ //draw horizontal lines
6499
+ let LTWH = tile.leftTopWidthHeight.slice();
6500
+ let sliceDim = 2; //
6501
+ let mm = this.frac2mm([0.5, 0.5, 0.5]);
6502
+ for (let i = 0; i < linesH.length; i++) {
6503
+ mm[sliceDim] = linesH[i];
6504
+ let frac = this.mm2frac(mm);
6505
+ this.drawLine([
6506
+ LTWH[0],
6507
+ LTWH[1] + LTWH[3] - frac[sliceDim] * LTWH[3],
6508
+ LTWH[2],
6509
+ 1,
6510
+ ]);
6511
+ }
6512
+ } //if horizontal lines
6513
+ if (linesV.length > 0) {
6514
+ //draw vertical lines
6515
+ let LTWH = tile.leftTopWidthHeight.slice();
6516
+ let isRadiolgical = tile.fovMM[0] < 0;
6517
+ let sliceDim = 0; //vertical lines on axial/coronal are L/R axis
6518
+ if (axCorSag === this.sliceTypeSagittal) sliceDim = 1; //vertical lines on sagittal are A/P
6519
+ let mm = this.frac2mm([0.5, 0.5, 0.5]);
6520
+ for (let i = 0; i < linesV.length; i++) {
6521
+ mm[sliceDim] = linesV[i];
6522
+ let frac = this.mm2frac(mm);
6523
+ if (isRadiolgical)
6524
+ this.drawLine([
6525
+ LTWH[0] + (LTWH[2] - frac[sliceDim] * LTWH[2]),
6526
+ LTWH[1],
6527
+ 1,
6528
+ LTWH[3],
6529
+ ]);
6530
+ else
6531
+ this.drawLine([
6532
+ LTWH[0] + frac[sliceDim] * LTWH[2],
6533
+ LTWH[1],
6534
+ 1,
6535
+ LTWH[3],
6536
+ ]);
6537
+ }
6538
+ } //if vertical lines
6539
+ };
6540
+
6541
+ Niivue.prototype.drawMosaic = function (mosaicStr) {
6542
+ if (this.volumes.length === 0) {
6543
+ console.log("Unable to draw mosaic until voxel-based image is loaded");
6544
+ return;
6545
+ }
6546
+ this.screenSlices = [];
6547
+ //render always in world space
6548
+ let fovRenderMM = this.screenFieldOfViewMM(this.sliceTypeAxial, true);
6549
+ //2d slices might be in world space or voxel space
6550
+ let fovSliceMM = this.screenFieldOfViewMM(this.sliceTypeAxial);
6551
+ //fovRender and fovSlice will only be different if scans are oblique and shown in voxel space
6552
+ //let mosaicStr = 'A -52 -12 C 8 ; S 28 48 66'
6553
+ let mxRow = 1;
6554
+ mosaicStr = mosaicStr.replaceAll(";", " ;").trim();
6555
+ let axiMM = [];
6556
+ let corMM = [];
6557
+ let sagMM = [];
6558
+ let items = mosaicStr.split(/\s+/);
6559
+ let scale = 1.0; //e.g. if 1.0 1mm per pixel
6560
+ let labelSize = this.opts.textHeight;
6561
+ let isCrossLinesUsed = false;
6562
+ for (let pass = 0; pass < 2; pass++) {
6563
+ //two pass: first calculate dimensions to determine scale, second draw items
6564
+ let isRender = false;
6565
+ let isCrossLines = false;
6566
+ isRender = false;
6567
+ let row = 0;
6568
+ let rowHt = 0;
6569
+ let left = 0;
6570
+ let top = 0;
6571
+ let mxRowWid = 0;
6572
+ let isLabel = false;
6573
+ let axCorSag = this.sliceTypeAxial;
6574
+ for (let i = 0; i < items.length; i++) {
6575
+ let item = items[i];
6576
+ if (item.includes("X")) {
6577
+ isCrossLines = true;
6578
+ continue;
6579
+ }
6580
+ if (item.includes("L")) {
6581
+ isLabel = !item.includes("-");
6582
+ continue;
6583
+ }
6584
+ if (item.includes("V") || item.includes("H")) {
6585
+ i++; //skip numeric value for vertical/horizontal overlap
6586
+ continue;
6587
+ }
6588
+ if (item.includes("A")) axCorSag = this.sliceTypeAxial;
6589
+ if (item.includes("C")) axCorSag = this.sliceTypeCoronal;
6590
+ if (item.includes("S")) axCorSag = this.sliceTypeSagittal;
6591
+ if (item.includes("R")) isRender = true;
6592
+ if (item.includes(";")) {
6593
+ //EOLN
6594
+ top += rowHt;
6595
+ mxRowWid = Math.max(mxRowWid, left);
6596
+ rowHt = 0;
6597
+ left = 0;
6598
+ }
6599
+ let sliceMM = parseFloat(item, NaN);
6600
+ if (isNaN(sliceMM)) continue;
6601
+ let w = 0;
6602
+ let h = 0;
6603
+ let fov = fovSliceMM;
6604
+ if (isRender) fov = fovRenderMM;
6605
+ //draw the slice
6606
+ if (axCorSag === this.sliceTypeSagittal) w = fov[1];
6607
+ else w = fov[0];
6608
+ if (axCorSag === this.sliceTypeAxial) h = fov[1];
6609
+ else h = fov[2];
6610
+ //console.log("w" + w + " h" + h + "::", fov);
6611
+ let leftTopWidthHeight = [left, top, w, h];
6612
+ if (pass === 0) {
6613
+ //1st pass: record slice locations in world space
6614
+ if (!isRender) {
6615
+ if (axCorSag === this.sliceTypeAxial) axiMM.push(sliceMM);
6616
+ if (axCorSag === this.sliceTypeCoronal) corMM.push(sliceMM);
6617
+ if (axCorSag === this.sliceTypeSagittal) sagMM.push(sliceMM);
6618
+ }
6619
+ } else {
6620
+ //2nd pass draw
6621
+ let ltwh = [scale * left, scale * top, scale * w, scale * h];
6622
+ this.opts.textHeight = isLabel ? labelSize : 0;
6623
+ if (isRender) {
6624
+ let inf = sliceMM < 0 ? -Infinity : Infinity;
6625
+ if (Object.is(sliceMM, -0)) inf = -Infinity; //catch negative zero
6626
+ this.draw2DMM(ltwh, axCorSag, inf, isLabel);
6627
+ } else this.draw2D(ltwh, axCorSag, sliceMM, isLabel);
6628
+ if (isCrossLines) {
6629
+ this.drawCrossLines(
6630
+ this.screenSlices.length - 1,
6631
+ axCorSag,
6632
+ axiMM,
6633
+ corMM,
6634
+ sagMM
6635
+ );
6636
+ isCrossLinesUsed = true;
6637
+ }
6638
+ isRender = false;
6639
+ isCrossLines = false;
6640
+ }
6641
+ left += w;
6642
+ rowHt = Math.max(rowHt, h);
6643
+ } //for each item in string
6644
+ top += rowHt;
6645
+ mxRowWid = Math.max(mxRowWid, left);
6646
+ if (mxRowWid <= 0 || top <= 0) break;
6647
+ let scaleW = this.gl.canvas.width / mxRowWid;
6648
+ let scaleH = this.effectiveCanvasHeight() / top;
6649
+ scale = Math.min(scaleW, scaleH);
6650
+ } //for pass 0 and 1
6651
+ this.opts.textHeight = labelSize;
6652
+ }; // drawMosaic()
6653
+
5598
6654
  // not included in public docs
5599
6655
  Niivue.prototype.drawScene = function () {
5600
6656
  if (!this.initialized) {
5601
6657
  return; // do not do anything until we are initialized (init will call drawScene).
5602
6658
  }
6659
+ this.colorbarHeight = 0;
5603
6660
  this.gl.clearColor(
5604
6661
  this.opts.backColor[0],
5605
6662
  this.opts.backColor[1],
@@ -5618,6 +6675,7 @@ Niivue.prototype.drawScene = function () {
5618
6675
  typeof this.volumes[0].dims === "undefined"
5619
6676
  ) {
5620
6677
  if (this.meshes.length > 0) {
6678
+ this.screenSlices = []; // empty array
5621
6679
  this.sliceType = this.sliceTypeRender; //only meshes loaded: we must use 3D render mode
5622
6680
  return this.draw3D(); //meshes loaded but no volume
5623
6681
  }
@@ -5625,95 +6683,122 @@ Niivue.prototype.drawScene = function () {
5625
6683
  return;
5626
6684
  }
5627
6685
  if (!this.back.hasOwnProperty("dims")) return;
5628
- if (this.sliceType === this.sliceTypeRender) {
5629
- //draw rendering
5630
- if (this.isDragging && this.scene.clipPlaneDepthAziElev[0] < 1.8) {
5631
- //if (this.scene.clipPlaneDepthAziElev[0] > 1.8) return;
5632
- let x = this.dragStart[0] - this.dragEnd[0];
5633
- let y = this.dragStart[1] - this.dragEnd[1];
5634
- let depthAziElev = this.dragClipPlaneStartDepthAziElev.slice();
5635
- depthAziElev[1] -= x;
5636
- depthAziElev[1] = depthAziElev[1] % 360;
5637
- depthAziElev[2] += y;
5638
- //gimbal lock: these next two lines could be changed - when we go over the pole, the Azimuth reverses
5639
- //if (depthAziElev[2] > 90) depthAziElev[2] = 90;
5640
- //if (depthAziElev[2] < -90) depthAziElev[2] = -90;
5641
- if (
5642
- depthAziElev[1] !== this.scene.clipPlaneDepthAziElev[1] ||
5643
- depthAziElev[2] !== this.scene.clipPlaneDepthAziElev[2]
5644
- ) {
5645
- this.scene.clipPlaneDepthAziElev = depthAziElev;
5646
- return this.setClipPlane(this.scene.clipPlaneDepthAziElev);
5647
- }
6686
+
6687
+ if (
6688
+ this.isDragging &&
6689
+ this.scene.clipPlaneDepthAziElev[0] < 1.8 &&
6690
+ this.inRenderTile(this.dragStart[0], this.dragStart[1]) >= 0
6691
+ ) {
6692
+ //user dragging over a 3D rendering
6693
+ let x = this.dragStart[0] - this.dragEnd[0];
6694
+ let y = this.dragStart[1] - this.dragEnd[1];
6695
+ let depthAziElev = this.dragClipPlaneStartDepthAziElev.slice();
6696
+ depthAziElev[1] -= x;
6697
+ depthAziElev[1] = depthAziElev[1] % 360;
6698
+ depthAziElev[2] += y;
6699
+ //gimbal lock: these next two lines could be changed - when we go over the pole, the Azimuth reverses
6700
+ //if (depthAziElev[2] > 90) depthAziElev[2] = 90;
6701
+ //if (depthAziElev[2] < -90) depthAziElev[2] = -90;
6702
+ if (
6703
+ depthAziElev[1] !== this.scene.clipPlaneDepthAziElev[1] ||
6704
+ depthAziElev[2] !== this.scene.clipPlaneDepthAziElev[2]
6705
+ ) {
6706
+ this.scene.clipPlaneDepthAziElev = depthAziElev;
6707
+ return this.setClipPlane(this.scene.clipPlaneDepthAziElev);
5648
6708
  }
5649
- return this.draw3D();
6709
+ } //dragging over rendering
6710
+ if (
6711
+ this.sliceMosaicString.length < 1 &&
6712
+ this.sliceType === this.sliceTypeRender
6713
+ ) {
6714
+ this.screenSlices = []; // empty array
6715
+ this.draw3D();
6716
+ if (this.opts.isColorbar) this.drawColorbar();
6717
+ return;
5650
6718
  }
5651
- let { volScale } = this.sliceScale();
5652
- this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
5653
- this.numScreenSlices = 0;
5654
- if (this.sliceType === this.sliceTypeAxial) {
5655
- //draw axial
5656
- let leftTopWidthHeight = this.scaleSlice(volScale[0], volScale[1]);
5657
- this.draw2D(leftTopWidthHeight, 0);
5658
- } else if (this.sliceType === this.sliceTypeCoronal) {
5659
- //draw coronal
5660
- let leftTopWidthHeight = this.scaleSlice(volScale[0], volScale[2]);
5661
- this.draw2D(leftTopWidthHeight, 1);
5662
- } else if (this.sliceType === this.sliceTypeSagittal) {
5663
- //draw sagittal
5664
- let leftTopWidthHeight = this.scaleSlice(volScale[1], volScale[2]);
5665
- this.draw2D(leftTopWidthHeight, 2);
6719
+ if (this.opts.isColorbar) this.reserveColorbarPanel();
6720
+ if (this.sliceMosaicString.length > 0) {
6721
+ this.drawMosaic(this.sliceMosaicString);
5666
6722
  } else {
5667
- //sliceTypeMultiplanar
5668
- let ltwh = this.scaleSlice(
5669
- volScale[0] + volScale[1],
5670
- volScale[1] + volScale[2]
5671
- );
5672
- let wX = (ltwh[2] * volScale[0]) / (volScale[0] + volScale[1]);
5673
- let ltwh3x1 = this.scaleSlice(
5674
- volScale[0] + volScale[0] + volScale[1],
5675
- Math.max(volScale[1], volScale[2])
5676
- );
5677
- let wX1 =
5678
- (ltwh3x1[2] * volScale[0]) / (volScale[0] + volScale[0] + volScale[1]);
5679
- if (wX1 > wX && !this.graph.autoSizeMultiplanar) {
5680
- //landscape screen ratio: 3 slices in single row
5681
- let pixScale = wX1 / volScale[0];
5682
- let hY1 = volScale[1] * pixScale;
5683
- let hZ1 = volScale[2] * pixScale;
5684
- //draw axial
5685
- this.draw2D([ltwh3x1[0], ltwh3x1[1], wX1, hY1], 0);
5686
- //draw coronal
5687
- this.draw2D([ltwh3x1[0] + wX1, ltwh3x1[1], wX1, hZ1], 1);
5688
- //draw sagittal
5689
- this.draw2D([ltwh3x1[0] + wX1 + wX1, ltwh3x1[1], hY1, hZ1], 2);
6723
+ // issue56 is use mm else use voxel
6724
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
6725
+ this.screenSlices = []; // empty array
6726
+ if (this.sliceType === this.sliceTypeAxial) {
6727
+ this.draw2D([0, 0, 0, 0], 0);
6728
+ } else if (this.sliceType === this.sliceTypeCoronal) {
6729
+ this.draw2D([0, 0, 0, 0], 1);
6730
+ } else if (this.sliceType === this.sliceTypeSagittal) {
6731
+ this.draw2D([0, 0, 0, 0], 2);
5690
6732
  } else {
5691
- let wY = ltwh[2] - wX;
5692
- let hY = (ltwh[3] * volScale[1]) / (volScale[1] + volScale[2]);
5693
- let hZ = ltwh[3] - hY;
5694
- //draw axial
5695
- this.draw2D([ltwh[0], ltwh[1] + hZ, wX, hY], 0);
5696
- //draw coronal
5697
- this.draw2D([ltwh[0], ltwh[1], wX, hZ], 1);
5698
- //draw sagittal
5699
- this.draw2D([ltwh[0] + wX, ltwh[1], wY, hZ], 2);
5700
- //draw colorbar (optional) // TODO currently only drawing one colorbar, there may be one per overlay + one for the background
5701
- var margin = this.opts.colorBarMargin * hY;
5702
- if (!this.graph.autoSizeMultiplanar) {
5703
- this.drawColorbar([
5704
- ltwh[0] + wX + margin,
5705
- ltwh[1] + hZ + margin,
5706
- wY - margin - margin,
5707
- hY * this.opts.colorbarHeight,
5708
- ]);
5709
- }
5710
- // drawTextBelow(gl, [ltwh[0]+ wX + (wY * 0.5), ltwh[1] + hZ + margin + hY * colorbarHeight], "Syzygy"); //DEMO
6733
+ //sliceTypeMultiplanar
6734
+ let { volScale, vox, longestAxis } = this.sliceScale();
6735
+ let ltwh = this.scaleSlice(
6736
+ volScale[0] + volScale[1],
6737
+ volScale[1] + volScale[2]
6738
+ );
6739
+ let wX = (ltwh[2] * volScale[0]) / (volScale[0] + volScale[1]);
6740
+ let ltwh3x1 = this.scaleSlice(
6741
+ volScale[0] + volScale[0] + volScale[1],
6742
+ Math.max(volScale[1], volScale[2])
6743
+ );
6744
+ let mx = Math.max(Math.max(volScale[1], volScale[2]), volScale[0]);
6745
+ let ltwh4x1 = this.scaleSlice(
6746
+ volScale[0] + volScale[0] + volScale[1] + mx,
6747
+ mx
6748
+ );
6749
+ let wX1 =
6750
+ (ltwh3x1[2] * volScale[0]) / (volScale[0] + volScale[0] + volScale[1]);
6751
+ if (wX1 > wX && !this.graph.autoSizeMultiplanar) {
6752
+ //landscape screen ratio: 3 slices in single row
6753
+ let pixScale = wX1 / volScale[0];
6754
+ let hY1 = volScale[1] * pixScale;
6755
+ let hZ1 = volScale[2] * pixScale;
6756
+ if (ltwh3x1[3] === ltwh4x1[3]) {
6757
+ ltwh3x1 = ltwh4x1;
6758
+ this.draw3D([
6759
+ ltwh3x1[0] + wX1 + wX1 + hY1,
6760
+ ltwh3x1[1],
6761
+ ltwh4x1[3],
6762
+ ltwh4x1[3],
6763
+ ]);
6764
+ }
6765
+ //draw axial
6766
+ this.draw2D([ltwh3x1[0], ltwh3x1[1], wX1, hY1], 0);
6767
+ //draw coronal
6768
+ this.draw2D([ltwh3x1[0] + wX1, ltwh3x1[1], wX1, hZ1], 1);
6769
+ //draw sagittal
6770
+ this.draw2D([ltwh3x1[0] + wX1 + wX1, ltwh3x1[1], hY1, hZ1], 2);
6771
+ } else {
6772
+ let wY = ltwh[2] - wX;
6773
+ let hY = (ltwh[3] * volScale[1]) / (volScale[1] + volScale[2]);
6774
+ let hZ = ltwh[3] - hY;
6775
+ //draw axial
6776
+ this.draw2D([ltwh[0], ltwh[1] + hZ, wX, hY], 0);
6777
+ //draw coronal
6778
+ this.draw2D([ltwh[0], ltwh[1], wX, hZ], 1);
6779
+ //draw sagittal
6780
+ this.draw2D([ltwh[0] + wX, ltwh[1], wY, hZ], 2);
6781
+ if (!this.graph.autoSizeMultiplanar)
6782
+ this.draw3D([ltwh[0] + wX, ltwh[1] + hZ, wY, hY]);
6783
+ } //if landscape else portrait
6784
+ } //if multiplanar
6785
+ } //if mosaic not 2D
6786
+ if (this.opts.isRuler) this.drawRuler();
6787
+ if (this.opts.isColorbar) this.drawColorbar();
6788
+
6789
+ if (this.isDragging) {
6790
+ if (this.opts.isDragShowsMeasurementTool) {
6791
+ this.drawMeasurementTool([
6792
+ this.dragStart[0],
6793
+ this.dragStart[1],
6794
+ this.dragEnd[0],
6795
+ this.dragEnd[1],
6796
+ ]);
6797
+ return;
5711
6798
  }
5712
- }
5713
- if (this.isDragging && this.sliceType !== this.sliceTypeRender) {
6799
+ if (this.inRenderTile(this.dragStart[0], this.dragStart[1]) >= 0) return;
5714
6800
  let width = Math.abs(this.dragStart[0] - this.dragEnd[0]);
5715
6801
  let height = Math.abs(this.dragStart[1] - this.dragEnd[1]);
5716
-
5717
6802
  this.drawSelectionBox([
5718
6803
  Math.min(this.dragStart[0], this.dragEnd[0]),
5719
6804
  Math.min(this.dragStart[1], this.dragEnd[1]),
@@ -5721,18 +6806,15 @@ Niivue.prototype.drawScene = function () {
5721
6806
  height,
5722
6807
  ]);
5723
6808
  }
5724
-
5725
6809
  const pos = this.frac2mm([
5726
6810
  this.scene.crosshairPos[0],
5727
6811
  this.scene.crosshairPos[1],
5728
6812
  this.scene.crosshairPos[2],
5729
6813
  ]);
5730
6814
  this.drawGraph();
5731
-
5732
6815
  posString =
5733
6816
  pos[0].toFixed(2) + "×" + pos[1].toFixed(2) + "×" + pos[2].toFixed(2);
5734
6817
  this.gl.finish();
5735
-
5736
6818
  this.readyForSync = true; // by the time we get here, all volumes should be loaded and ready to be drawn. We let other niivue instances know that we can now reliably sync draw calls (images are loaded)
5737
6819
  return posString;
5738
6820
  }; // drawScene()