@niivue/niivue 0.26.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/niivue.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Shader } from "./shader.js";
2
2
  import * as mat from "gl-matrix";
3
3
  import {
4
+ vertOrientCubeShader,
5
+ fragOrientCubeShader,
4
6
  vertSliceMMShader,
5
7
  fragSliceMMShader,
6
8
  vertSliceShader,
@@ -44,10 +46,10 @@ import {
44
46
  fragFiberShader,
45
47
  vertSurfaceShader,
46
48
  fragSurfaceShader,
47
- fragDepthPickingShader,
48
49
  fragVolumePickingShader,
49
50
  } from "./shader-srcs.js";
50
51
  import { Subject } from "rxjs";
52
+ import { orientCube } from "./orientCube.js";
51
53
  import { NiivueObject3D } from "./niivue-object3D.js";
52
54
  import { NiivueShader3D } from "./niivue-shader3D";
53
55
  import { NVImage } from "./nvimage.js";
@@ -112,17 +114,23 @@ export function Niivue(options = {}) {
112
114
  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.
113
115
  clipPlaneHotKey: "KeyC", // keyboard short cut to activate the clip plane
114
116
  viewModeHotKey: "KeyV", // keyboard shortcut to switch view modes
117
+ doubleTouchTimeout: 500,
118
+ longTouchTimeout: 1000,
115
119
  keyDebounceTime: 50, // default debounce time used in keyup listeners
116
120
  isNearestInterpolation: false,
117
121
  isAtlasOutline: false,
118
122
  isRuler: false,
119
123
  isColorbar: false,
124
+ isOrientCube: false,
125
+ multiplanarPadPixels: 0,
120
126
  isRadiologicalConvention: false,
121
127
  meshThicknessOn2D: Infinity,
122
128
  isDragShowsMeasurementTool: false,
129
+ isDepthPickMesh: false,
123
130
  isCornerOrientationText: false,
124
131
  sagittalNoseLeft: false, //sagittal slices can have Y+ going left or right
125
132
  isSliceMM: false,
133
+ isHighResolutionCapable: false,
126
134
  logging: false,
127
135
  loadingText: "waiting for images...",
128
136
  dragAndDropEnabled: true,
@@ -130,9 +138,14 @@ export function Niivue(options = {}) {
130
138
  penValue: 1, // sets drawing color. see "drawPt"
131
139
  isFilledPen: false,
132
140
  thumbnail: "",
141
+ maxDrawUndoBitmaps: 8,
133
142
  onLocationChange: () => {},
134
143
  onIntensityChange: () => {},
135
144
  onImageLoaded: () => {},
145
+ onError: () => {},
146
+ onInfo: () => {},
147
+ onWarn: () => {},
148
+ onDebug: () => {},
136
149
  };
137
150
 
138
151
  this.canvas = null; // the canvas element on the page
@@ -141,8 +154,6 @@ export function Niivue(options = {}) {
141
154
  this.volumeTexture = null;
142
155
  this.drawTexture = null; //the GPU memory storage of the drawing
143
156
  this.drawBitmap = null; //the CPU memory storage of the drawing
144
- this.maxDrawUndoBitmaps = 8; //analogy: number of bullets in revolver, we will cycle these
145
- this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //analogy: cylinder position of a revolver
146
157
  this.drawUndoBitmaps = [];
147
158
  this.drawOpacity = 0.8;
148
159
  this.colorbarHeight = 0; //height in pixels, set when colorbar is drawn
@@ -154,10 +165,13 @@ export function Niivue(options = {}) {
154
165
  this.overlayTextureID = [];
155
166
  this.sliceShader = null;
156
167
  this.sliceMMShader = null;
168
+ this.orientCubeShader = null;
169
+ this.orientCubeShaderVAO = null;
157
170
  this.rectShader = null;
158
171
  this.graphShader = null;
159
172
  this.renderShader = null;
160
- this.pickingShader = null;
173
+ this.pickingMeshShader = null;
174
+ this.pickingImageShader = null;
161
175
  this.colorbarShader = null;
162
176
  this.fontShader = null;
163
177
  this.fontTexture = null;
@@ -177,8 +191,6 @@ export function Niivue(options = {}) {
177
191
  this.genericVAO = null; //used for 2D slices, 2D lines, 2D Fonts
178
192
  this.unusedVAO = null;
179
193
  this.crosshairs3D = null;
180
- this.pickingSurfaceShader = null;
181
-
182
194
  this.DEFAULT_FONT_GLYPH_SHEET = defaultFontPNG; //"/fonts/Roboto-Regular.png";
183
195
  this.DEFAULT_FONT_METRICS = defaultFontMetrics; //"/fonts/Roboto-Regular.json";
184
196
  this.fontMets = null;
@@ -212,6 +224,10 @@ export function Niivue(options = {}) {
212
224
  this.scene.prevY = 0;
213
225
  this.scene.currX = 0;
214
226
  this.scene.currY = 0;
227
+ this.currentTouchTime = 0;
228
+ this.lastTouchTime = 0;
229
+ this.touchTimer = null;
230
+ this.doubleTouch = false;
215
231
  this.back = {}; // base layer; defines image space to work in. Defined as this.volumes[0] in Niivue.loadVolumes
216
232
  this.overlays = []; // layers added on top of base image (e.g. masks or stat maps). Essentially everything after this.volumes[0] is an overlay. So is this necessary?
217
233
  this.volumes = []; // all loaded images. Can add in the ability to push or slice as needed
@@ -298,6 +314,9 @@ export function Niivue(options = {}) {
298
314
  options[prop] === undefined ? this.defaults[prop] : options[prop];
299
315
  }
300
316
 
317
+ // now that opts have been parsed, set the current undo to max undo
318
+ this.currentDrawUndoBitmap = this.opts.maxDrawUndoBitmaps; //analogy: cylinder position of a revolver
319
+
301
320
  if (this.opts.drawingEnabled) {
302
321
  this.createEmptyDrawing();
303
322
  }
@@ -639,9 +658,13 @@ Niivue.prototype.arrayEquals = function (a, b) {
639
658
  Niivue.prototype.resizeListener = function () {
640
659
  this.canvas.style.width = "100%";
641
660
  this.canvas.style.height = "100%";
642
- this.canvas.width = this.canvas.offsetWidth;
643
- this.canvas.height = this.canvas.offsetHeight;
644
-
661
+ let dpr = 1;
662
+ //https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
663
+ //https://www.khronos.org/webgl/wiki/HandlingHighDPI
664
+ if (this.opts.isHighResolutionCapable) dpr = window.devicePixelRatio || 1;
665
+ this.canvas.width = this.canvas.offsetWidth * dpr;
666
+ this.canvas.height = this.canvas.offsetHeight * dpr;
667
+ console.log("RESIZE" + dpr);
645
668
  this.drawScene();
646
669
  };
647
670
 
@@ -857,7 +880,31 @@ Niivue.prototype.checkMultitouch = function (e) {
857
880
  // note: no test yet
858
881
  Niivue.prototype.touchStartListener = function (e) {
859
882
  e.preventDefault();
883
+ if (!this.touchTimer) {
884
+ this.touchTimer = setTimeout(() => {
885
+ //this.drawScene()
886
+ this.resetBriCon(e);
887
+ }, this.opts.longTouchTimeout);
888
+ }
860
889
  this.scene.touchdown = true;
890
+ this.currentTouchTime = new Date().getTime();
891
+ let timeSinceTouch = this.currentTouchTime - this.lastTouchTime;
892
+ if (timeSinceTouch < this.opts.doubleTouchTimeout && timeSinceTouch > 0) {
893
+ this.doubleTouch = true;
894
+ this.dragStart[0] =
895
+ e.targetTouches[0].clientX - e.target.getBoundingClientRect().left;
896
+ this.dragStart[1] =
897
+ e.targetTouches[0].clientY - e.target.getBoundingClientRect().top;
898
+ this.resetBriCon(e);
899
+ this.lastTouchTime = this.currentTouchTime;
900
+ return;
901
+ } else {
902
+ // reset values to be ready for next touch
903
+ this.doubleTouch = false;
904
+ this.dragStart = [0, 0];
905
+ this.dragEnd = [0, 0];
906
+ this.lastTouchTime = this.currentTouchTime;
907
+ }
861
908
  if (this.scene.touchdown && e.touches.length < 2) {
862
909
  this.multiTouchGesture = false;
863
910
  } else {
@@ -870,10 +917,22 @@ Niivue.prototype.touchStartListener = function (e) {
870
917
  // not included in public docs
871
918
  // handler for touchend (finger lift off screen)
872
919
  // note: no test yet
873
- Niivue.prototype.touchEndListener = function () {
920
+ Niivue.prototype.touchEndListener = function (e) {
921
+ e.preventDefault();
874
922
  this.scene.touchdown = false;
875
923
  this.lastTwoTouchDistance = 0;
876
924
  this.multiTouchGesture = false;
925
+ if (this.touchTimer) {
926
+ clearTimeout(this.touchTimer);
927
+ this.touchTimer = null;
928
+ }
929
+ if (this.isDragging) {
930
+ this.isDragging = false;
931
+ if (this.opts.isDragShowsMeasurementTool) return;
932
+ this.calculateNewRange();
933
+ this.refreshLayers(this.volumes[0], 0, this.volumes.length);
934
+ }
935
+ this.drawScene();
877
936
  };
878
937
 
879
938
  // not included in public docs
@@ -906,22 +965,34 @@ Niivue.prototype.mouseMoveListener = function (e) {
906
965
  Niivue.prototype.resetBriCon = function (msg = null) {
907
966
  //this.volumes[0].cal_min = this.volumes[0].global_min;
908
967
  //this.volumes[0].cal_max = this.volumes[0].global_max;
909
-
910
968
  // don't reset bri/con if the user is in 3D mode and double clicks
969
+ if (this.isDragging) return;
911
970
  let isRender = false;
912
- if (this.sliceType === this.sliceTypeRender) {
913
- isRender = true;
914
- }
971
+ if (this.sliceType === this.sliceTypeRender) isRender = true;
972
+ let x = 0;
973
+ let y = 0;
915
974
  if (msg !== null) {
975
+ // if a touch event
976
+ if (msg.targetTouches !== undefined) {
977
+ x =
978
+ msg.targetTouches[0].clientX - msg.target.getBoundingClientRect().left;
979
+ y = msg.targetTouches[0].clientY - msg.target.getBoundingClientRect().top;
980
+ } else {
981
+ // if a mouse event
982
+ x = msg.offsetX;
983
+ y = msg.offsetY;
984
+ }
916
985
  // test if render is one of the tiles
917
- if (this.inRenderTile(msg.offsetX, msg.offsetY) >= 0) isRender = true;
986
+ if (this.inRenderTile(x, y) >= 0) isRender = true;
918
987
  }
988
+
919
989
  if (isRender) {
920
990
  this.scene.mouseDepthPicker = true;
921
991
  this.drawScene();
922
- this.drawScene();
992
+ this.drawScene(); // this duplicate drawScene is necessary for deptch picking. DO NOT REMOVE
923
993
  return;
924
994
  }
995
+ if (this.doubleTouch) return;
925
996
  this.volumes[0].cal_min = this.volumes[0].robust_min;
926
997
  this.volumes[0].cal_max = this.volumes[0].robust_max;
927
998
  this.opts.onIntensityChange(this.volumes[0]);
@@ -935,6 +1006,15 @@ Niivue.prototype.resetBriCon = function (msg = null) {
935
1006
  Niivue.prototype.touchMoveListener = function (e) {
936
1007
  if (this.scene.touchdown && e.touches.length < 2) {
937
1008
  var rect = this.canvas.getBoundingClientRect();
1009
+ this.isDragging = true;
1010
+ if (this.doubleTouch && this.isDragging) {
1011
+ this.dragEnd[0] =
1012
+ e.targetTouches[0].clientX - e.target.getBoundingClientRect().left;
1013
+ this.dragEnd[1] =
1014
+ e.targetTouches[0].clientY - e.target.getBoundingClientRect().top;
1015
+ this.drawScene();
1016
+ return;
1017
+ }
938
1018
  this.mouseClick(
939
1019
  e.touches[0].clientX - rect.left,
940
1020
  e.touches[0].clientY - rect.top
@@ -1096,6 +1176,10 @@ Niivue.prototype.wheelListener = function (e) {
1096
1176
  // scroll 2D slices
1097
1177
  e.preventDefault();
1098
1178
  e.stopPropagation();
1179
+ // if thumbnailVisible this do not activate a canvas interaction when scrolling
1180
+ if (this.thumbnailVisible) {
1181
+ return;
1182
+ }
1099
1183
  var rect = this.canvas.getBoundingClientRect();
1100
1184
  if (e.deltaY < 0) {
1101
1185
  this.sliceScroll2D(-0.01, e.clientX - rect.left, e.clientY - rect.top);
@@ -1373,6 +1457,13 @@ Niivue.prototype.getRadiologicalConvention = function () {
1373
1457
  return this.opts.isRadiologicalConvention;
1374
1458
  };
1375
1459
 
1460
+ Niivue.prototype.setHighResolutionCapable = function (isHighResolutionCapable) {
1461
+ this.opts.isHighResolutionCapable = isHighResolutionCapable;
1462
+ console.log("HighDPI feature is experimental");
1463
+ this.resizeListener(); // test isHighResolutionCapable
1464
+ this.drawScene();
1465
+ };
1466
+
1376
1467
  /**
1377
1468
  * add a new volume to the canvas
1378
1469
  * @param {NVImage} volume the new volume to add to the canvas
@@ -1541,13 +1632,13 @@ Niivue.prototype.drawAddUndoBitmap = async function (fnm) {
1541
1632
  //let rle = encodeRLE(this.drawBitmap);
1542
1633
  //the bitmaps are a cyclical loop, like a revolver hand gun: increment the cylinder
1543
1634
  this.currentDrawUndoBitmap++;
1544
- if (this.currentDrawUndoBitmap >= this.maxDrawUndoBitmaps)
1635
+ if (this.currentDrawUndoBitmap >= this.opts.maxDrawUndoBitmaps)
1545
1636
  this.currentDrawUndoBitmap = 0;
1546
1637
  this.drawUndoBitmaps[this.currentDrawUndoBitmap] = encodeRLE(this.drawBitmap);
1547
1638
  }; // drawAddUndoBitmap()
1548
1639
 
1549
1640
  Niivue.prototype.drawClearAllUndoBitmaps = async function () {
1550
- this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //next add will be cylinder 0
1641
+ this.currentDrawUndoBitmap = this.opts.maxDrawUndoBitmaps; //next add will be cylinder 0
1551
1642
  if (this.drawUndoBitmaps.length < 1) return;
1552
1643
  for (let i = this.drawUndoBitmaps.length - 1; i >= 0; i--) array[i] = [];
1553
1644
  }; // drawClearAllUndoBitmaps()
@@ -3322,19 +3413,27 @@ Niivue.prototype.init = async function () {
3322
3413
  this.gl.enableVertexAttribArray(0);
3323
3414
  this.gl.vertexAttribPointer(0, 3, this.gl.FLOAT, false, 0, 0);
3324
3415
  this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
3325
- this.pickingShader = new Shader(
3416
+
3417
+ this.pickingMeshShader = new Shader(
3418
+ this.gl,
3419
+ vertMeshShader,
3420
+ fragMeshDepthShader
3421
+ );
3422
+ this.pickingImageShader = new Shader(
3326
3423
  this.gl,
3327
3424
  vertRenderShader,
3328
3425
  fragVolumePickingShader
3329
3426
  );
3330
- this.pickingShader.use(this.gl);
3331
- this.gl.uniform1i(this.pickingShader.uniforms["volume"], 0);
3427
+ this.pickingImageShader.use(this.gl);
3428
+ this.gl.uniform1i(this.pickingImageShader.uniforms["volume"], 0);
3332
3429
  //this.gl.uniform1i(pickingShader.uniforms["colormap"], 1); //orient shader applies colormap
3333
- this.gl.uniform1i(this.pickingShader.uniforms["overlay"], 2);
3334
- this.pickingShader.mvpUniformLoc = this.pickingShader.uniforms["mvpMtx"];
3335
- this.pickingShader.rayDirUniformLoc = this.pickingShader.uniforms["rayDir"];
3336
- this.pickingShader.clipPlaneUniformLoc =
3337
- this.pickingShader.uniforms["clipPlane"];
3430
+ this.gl.uniform1i(this.pickingImageShader.uniforms["overlay"], 2);
3431
+ this.pickingImageShader.mvpUniformLoc =
3432
+ this.pickingImageShader.uniforms["mvpMtx"];
3433
+ this.pickingImageShader.rayDirUniformLoc =
3434
+ this.pickingImageShader.uniforms["rayDir"];
3435
+ this.pickingImageShader.clipPlaneUniformLoc =
3436
+ this.pickingImageShader.uniforms["clipPlane"];
3338
3437
  // slice shader
3339
3438
  this.sliceShader = new Shader(this.gl, vertSliceShader, fragSliceShader);
3340
3439
  this.sliceShader.use(this.gl);
@@ -3356,6 +3455,29 @@ Niivue.prototype.init = async function () {
3356
3455
  this.sliceMMShader.uniforms["drawOpacity"],
3357
3456
  this.drawOpacity
3358
3457
  );
3458
+ //orient cube
3459
+ this.orientCubeShader = new Shader(
3460
+ this.gl,
3461
+ vertOrientCubeShader,
3462
+ fragOrientCubeShader
3463
+ );
3464
+ let gl = this.gl;
3465
+ this.orientCubeShaderVAO = gl.createVertexArray();
3466
+ gl.bindVertexArray(this.orientCubeShaderVAO);
3467
+ let program = this.orientCubeShader.program;
3468
+ this.orientCubeMtxLoc = gl.getUniformLocation(program, "u_matrix");
3469
+ // Create a buffer
3470
+ var positionBuffer = gl.createBuffer();
3471
+ gl.enableVertexAttribArray(0);
3472
+ gl.enableVertexAttribArray(1);
3473
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
3474
+ gl.bufferData(gl.ARRAY_BUFFER, orientCube, gl.STATIC_DRAW);
3475
+ //XYZ position: (three floats)
3476
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
3477
+ //RGB color: (also three floats)
3478
+ gl.enableVertexAttribArray(1);
3479
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
3480
+ gl.bindVertexArray(this.unusedVAO);
3359
3481
  // rect shader (crosshair): horizontal and vertical lines only
3360
3482
  this.rectShader = new Shader(this.gl, vertRectShader, fragRectShader);
3361
3483
  // line shader: diagonal lines
@@ -3420,13 +3542,6 @@ Niivue.prototype.init = async function () {
3420
3542
  vertOrientShader,
3421
3543
  fragOrientShaderU.concat(fragRGBOrientShader)
3422
3544
  );
3423
-
3424
- this.pickingSurfaceShader = new Shader(
3425
- this.gl,
3426
- vertRenderShader,
3427
- fragDepthPickingShader
3428
- );
3429
-
3430
3545
  // 3D crosshair cylinder
3431
3546
  this.surfaceShader = new Shader(
3432
3547
  this.gl,
@@ -3453,6 +3568,7 @@ Niivue.prototype.init = async function () {
3453
3568
  }
3454
3569
  this.updateGLVolume();
3455
3570
  this.initialized = true;
3571
+ if (this.opts.isHighResolutionCapable) this.resizeListener();
3456
3572
  this.drawScene();
3457
3573
  return this;
3458
3574
  }; // init()
@@ -3606,8 +3722,7 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
3606
3722
  this.gl.uniform3fv(this.renderShader.uniforms["texVox"], vox);
3607
3723
  this.gl.uniform3fv(this.renderShader.uniforms["volScale"], volScale);
3608
3724
  // add shader to object
3609
- let volumeRenderShader = this.renderShader;
3610
- let pickingShader = this.pickingShader;
3725
+ let pickingShader = this.pickingImageShader;
3611
3726
  pickingShader.use(this.gl);
3612
3727
  this.gl.uniform1i(pickingShader.uniforms["volume"], 0);
3613
3728
  this.gl.uniform1i(pickingShader.uniforms["colormap"], 1);
@@ -4031,12 +4146,12 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
4031
4146
  );
4032
4147
  this.gl.uniform3fv(this.renderShader.uniforms["texVox"], vox);
4033
4148
  this.gl.uniform3fv(this.renderShader.uniforms["volScale"], volScale);
4034
- this.pickingShader.use(this.gl);
4149
+ this.pickingImageShader.use(this.gl);
4035
4150
  this.gl.uniform1f(
4036
- this.pickingShader.uniforms["overlays"],
4151
+ this.pickingImageShader.uniforms["overlays"],
4037
4152
  this.overlays.length
4038
4153
  );
4039
- this.gl.uniform3fv(this.pickingShader.uniforms["texVox"], vox);
4154
+ this.gl.uniform3fv(this.pickingImageShader.uniforms["texVox"], vox);
4040
4155
  this.sliceMMShader.use(this.gl);
4041
4156
  this.gl.uniform1f(
4042
4157
  this.sliceMMShader.uniforms["overlays"],
@@ -4577,8 +4692,6 @@ function tickSpacing(mn, mx) {
4577
4692
  let range = Math.abs(mx - mn);
4578
4693
  let [spacing, ticMin] = tickSpacingX(5, mn, mx);
4579
4694
  if (range % spacing === 0) return [spacing, ticMin];
4580
- //[spacing, ticMin] = tickSpacingX(6, mn, mx);
4581
- //if ((range % spacing) === 0) return [spacing, ticMin];
4582
4695
  [spacing, ticMin] = tickSpacingX(4, mn, mx);
4583
4696
  if (range % spacing === 0) return [spacing, ticMin];
4584
4697
  [spacing, ticMin] = tickSpacingX(5, mn, mx);
@@ -4706,7 +4819,7 @@ Niivue.prototype.drawColorbarCore = function (
4706
4819
  //this.drawTextRight([plotLTWH[0], y], str, fntScale)
4707
4820
  tic += spacing;
4708
4821
  }
4709
- };
4822
+ }; // drawColorbarCore()
4710
4823
 
4711
4824
  Niivue.prototype.drawColorbar = function () {
4712
4825
  let nlayers = this.volumes.length;
@@ -4907,8 +5020,7 @@ Niivue.prototype.calculateMvpMatrix2D = function (
4907
5020
  let gl = this.gl;
4908
5021
  gl.viewport(
4909
5022
  leftTopWidthHeight[0],
4910
- this.gl.canvas.clientHeight -
4911
- (leftTopWidthHeight[1] + leftTopWidthHeight[3]), //lower numbers near bottom
5023
+ this.gl.canvas.height - (leftTopWidthHeight[1] + leftTopWidthHeight[3]), //lower numbers near bottom
4912
5024
  leftTopWidthHeight[2],
4913
5025
  leftTopWidthHeight[3]
4914
5026
  );
@@ -5419,11 +5531,6 @@ Niivue.prototype.calculateMvpMatrix = function (
5419
5531
  mat.mat4.invert(iModelMatrix, modelMatrix);
5420
5532
  let normalMatrix = mat.mat4.create();
5421
5533
  mat.mat4.transpose(normalMatrix, iModelMatrix);
5422
- //this.gl.uniformMatrix4fv(this.meshShader.uniforms["mvpMtx"], false, m);
5423
-
5424
- // transpose(out, a) - > {mat4}
5425
- // invert(out, a)
5426
- // normalMatrix := modelMatrix.Inverse.Transpose;
5427
5534
  let modelViewProjectionMatrix = mat.mat4.create();
5428
5535
  mat.mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
5429
5536
  return [modelViewProjectionMatrix, modelMatrix, normalMatrix];
@@ -5480,9 +5587,7 @@ Niivue.prototype.calculateRayDirection = function (azimuth, elevation) {
5480
5587
  return rayDir;
5481
5588
  }; // calculateRayDirection
5482
5589
 
5483
- Niivue.prototype.setPivot3D = function () {
5484
- //compute extents of all volumes and meshes in scene
5485
- // pivot around center of these.
5590
+ Niivue.prototype.sceneExtentsMinMax = function () {
5486
5591
  let mn = mat.vec3.fromValues(0, 0, 0);
5487
5592
  let mx = mat.vec3.fromValues(0, 0, 0);
5488
5593
  if (this.volumes.length > 0) {
@@ -5520,6 +5625,15 @@ Niivue.prototype.setPivot3D = function () {
5520
5625
  mat.vec3.max(mx, mx, v);
5521
5626
  }
5522
5627
  }
5628
+ let range = mat.vec3.create();
5629
+ mat.vec3.subtract(range, mx, mn);
5630
+ return [mn, mx, range];
5631
+ };
5632
+
5633
+ Niivue.prototype.setPivot3D = function () {
5634
+ //compute extents of all volumes and meshes in scene
5635
+ // pivot around center of these.
5636
+ let [mn, mx] = this.sceneExtentsMinMax();
5523
5637
  let pivot = mat.vec3.create();
5524
5638
  //pivot is half way between min and max:
5525
5639
  mat.vec3.add(pivot, mn, mx);
@@ -5712,6 +5826,7 @@ Niivue.prototype.drawGraph = function () {
5712
5826
  graph.backColor[3] * 2,
5713
5827
  ]);
5714
5828
  //draw horizontal lines
5829
+ // let [spacing, ticMin] = tickSpacing(mn, mx);
5715
5830
  let rangeH = mx - mn;
5716
5831
  let scaleH = plotLTWH[3] / rangeH;
5717
5832
  let scaleW = plotLTWH[2] / (graph.lines[0].length - 1);
@@ -5759,7 +5874,7 @@ Niivue.prototype.drawGraph = function () {
5759
5874
  thinColor,
5760
5875
  thick
5761
5876
  );
5762
- } else if (i > 0) {
5877
+ } else {
5763
5878
  let str = humanize(i);
5764
5879
  if (fntSize > 0)
5765
5880
  this.drawTextBelow(
@@ -5824,6 +5939,171 @@ function isRadiological(mtx) {
5824
5939
  return vRotated[0];
5825
5940
  } //n.b. ambiguous for pure sagittal views
5826
5941
 
5942
+ //function unProject(winX, winY, winZ, model, proj, view) {
5943
+ function unProject(winX, winY, winZ, mvpMatrix) {
5944
+ //https://github.com/bringhurst/webgl-unproject
5945
+ let inp = mat.vec4.fromValues(winX, winY, winZ, 1.0);
5946
+ let finalMatrix = mat.mat4.clone(mvpMatrix);
5947
+ //mat.mat4.multiply(finalMatrix, model, proj);
5948
+ mat.mat4.invert(finalMatrix, finalMatrix);
5949
+ //view is leftTopWidthHeight
5950
+ /* Map to range -1 to 1 */
5951
+ inp[0] = inp[0] * 2 - 1;
5952
+ inp[1] = inp[1] * 2 - 1;
5953
+ inp[2] = inp[2] * 2 - 1;
5954
+ let out = mat.vec4.create();
5955
+ mat.vec4.transformMat4(out, inp, finalMatrix);
5956
+ if (out[3] === 0.0) return out; //error
5957
+ out[0] /= out[3];
5958
+ out[1] /= out[3];
5959
+ out[2] /= out[3];
5960
+ return out;
5961
+ }
5962
+
5963
+ function unpackFloatFromVec4i(val) {
5964
+ //Convert 32-bit rgba to float32
5965
+ //https://github.com/rii-mango/Papaya/blob/782a19341af77a510d674c777b6da46afb8c65f1/src/js/viewer/screensurface.js#L552
5966
+ var bitSh = [
5967
+ 1.0 / (256.0 * 256.0 * 256.0),
5968
+ 1.0 / (256.0 * 256.0),
5969
+ 1.0 / 256.0,
5970
+ 1.0,
5971
+ ];
5972
+ return (
5973
+ (val[0] * bitSh[0] +
5974
+ val[1] * bitSh[1] +
5975
+ val[2] * bitSh[2] +
5976
+ val[3] * bitSh[3]) /
5977
+ 255.0
5978
+ );
5979
+ }
5980
+
5981
+ Niivue.prototype.depthPicker = function (leftTopWidthHeight, mvpMatrix) {
5982
+ //use color of screen pixel to infer X,Y,Z coordinates
5983
+ if (!this.scene.mouseDepthPicker) return;
5984
+ //start PICKING: picking shader and reading values is slow
5985
+ this.scene.mouseDepthPicker = false;
5986
+ let gl = this.gl;
5987
+ const pixelX =
5988
+ (this.mousePos[0] * leftTopWidthHeight[2]) / leftTopWidthHeight[2];
5989
+ const pixelY =
5990
+ gl.canvas.height -
5991
+ (this.mousePos[1] * leftTopWidthHeight[3]) / leftTopWidthHeight[3] -
5992
+ 1;
5993
+ console.log(pixelX + "/" + pixelY);
5994
+ const rgbaPixel = new Uint8Array(4);
5995
+ gl.readPixels(
5996
+ pixelX, // x
5997
+ pixelY, // y
5998
+ 1, // width
5999
+ 1, // height
6000
+ gl.RGBA, // format
6001
+ gl.UNSIGNED_BYTE, // type
6002
+ rgbaPixel
6003
+ ); // typed array to hold result
6004
+ this.selectedObjectId = rgbaPixel[3];
6005
+ if (this.selectedObjectId === this.VOLUME_ID) {
6006
+ this.scene.crosshairPos = new Float32Array(rgbaPixel.slice(0, 3)).map(
6007
+ (x) => x / 255.0
6008
+ );
6009
+ return;
6010
+ }
6011
+ let depthZ = unpackFloatFromVec4i(rgbaPixel);
6012
+ if (depthZ > 1.0) return;
6013
+ let fracX =
6014
+ (this.mousePos[0] - leftTopWidthHeight[0]) / leftTopWidthHeight[2];
6015
+ let fracY =
6016
+ (gl.canvas.height - this.mousePos[1] - leftTopWidthHeight[1]) /
6017
+ leftTopWidthHeight[3];
6018
+ //todo: check when top is not zero: leftTopWidthHeight[1]
6019
+ let mm = unProject(fracX, fracY, depthZ, mvpMatrix);
6020
+ let frac = this.mm2frac(mm);
6021
+ if (
6022
+ frac[0] < 0 ||
6023
+ frac[0] > 1 ||
6024
+ frac[1] < 0 ||
6025
+ frac[1] > 1 ||
6026
+ frac[2] < 0 ||
6027
+ frac[2] > 1
6028
+ )
6029
+ return;
6030
+ this.scene.crosshairPos = this.mm2frac(mm);
6031
+ }; // depthPicker()
6032
+
6033
+ Niivue.prototype.drawImage3D = function (mvpMatrix, azimuth, elevation) {
6034
+ if (this.volumes.length === 0) return;
6035
+ let gl = this.gl;
6036
+ const rayDir = this.calculateRayDirection(azimuth, elevation);
6037
+ let object3D = this.volumeObject3D;
6038
+ if (object3D) {
6039
+ gl.enable(gl.BLEND);
6040
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6041
+ gl.enable(gl.CULL_FACE);
6042
+ gl.cullFace(gl.FRONT); //TH switch since we L/R flipped in calculateMvpMatrix
6043
+ let shader = this.renderShader;
6044
+ //shader = this.pickingImageShader;
6045
+ if (this.scene.mouseDepthPicker) shader = this.pickingImageShader;
6046
+ shader.use(this.gl);
6047
+ gl.uniform1i(
6048
+ shader.uniforms["backgroundMasksOverlays"],
6049
+ this.backgroundMasksOverlays
6050
+ );
6051
+ gl.uniformMatrix4fv(shader.mvpUniformLoc, false, mvpMatrix);
6052
+ gl.uniformMatrix4fv(shader.mvpMatRASLoc, false, this.back.matRAS);
6053
+ gl.uniform3fv(shader.rayDirUniformLoc, rayDir);
6054
+ gl.uniform4fv(shader.clipPlaneUniformLoc, this.scene.clipPlane);
6055
+ gl.bindVertexArray(object3D.vao);
6056
+ gl.drawElements(object3D.mode, object3D.indexCount, gl.UNSIGNED_SHORT, 0);
6057
+ gl.bindVertexArray(this.unusedVAO);
6058
+ }
6059
+ }; // drawImage3D()
6060
+
6061
+ Niivue.prototype.drawOrientationCube = function (
6062
+ leftTopWidthHeight,
6063
+ azimuth = 0,
6064
+ elevation = 0
6065
+ ) {
6066
+ if (!this.opts.isOrientCube) return;
6067
+ let sz = 0.05 * Math.min(leftTopWidthHeight[2], leftTopWidthHeight[3]);
6068
+ if (sz < 5) return;
6069
+ let gl = this.gl;
6070
+ gl.enable(gl.CULL_FACE);
6071
+ gl.cullFace(gl.BACK);
6072
+ this.orientCubeShader.use(this.gl);
6073
+ gl.bindVertexArray(this.orientCubeShaderVAO);
6074
+ function deg2rad(deg) {
6075
+ return deg * (Math.PI / 180.0);
6076
+ }
6077
+ let modelMatrix = mat.mat4.create();
6078
+ let projectionMatrix = mat.mat4.create();
6079
+ //ortho(out, left, right, bottom, top, near, far)
6080
+ mat.mat4.ortho(
6081
+ projectionMatrix,
6082
+ 0,
6083
+ gl.canvas.width,
6084
+ 0,
6085
+ gl.canvas.height,
6086
+ -10 * sz,
6087
+ 10 * sz
6088
+ );
6089
+ //translate position left and right
6090
+ mat.mat4.translate(modelMatrix, modelMatrix, [
6091
+ 1.8 * sz + leftTopWidthHeight[0],
6092
+ 1.8 * sz + leftTopWidthHeight[1],
6093
+ 0,
6094
+ ]);
6095
+ mat.mat4.scale(modelMatrix, modelMatrix, [sz, sz, sz]);
6096
+ //apply elevation
6097
+ mat.mat4.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
6098
+ //apply azimuth
6099
+ mat.mat4.rotateZ(modelMatrix, modelMatrix, deg2rad(-azimuth));
6100
+ let modelViewProjectionMatrix = mat.mat4.create();
6101
+ mat.mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
6102
+ gl.uniformMatrix4fv(this.orientCubeMtxLoc, false, modelViewProjectionMatrix);
6103
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 168);
6104
+ gl.bindVertexArray(this.unusedVAO);
6105
+ }; // drawOrientationCube()
6106
+
5827
6107
  // not included in public docs
5828
6108
  Niivue.prototype.draw3D = function (
5829
6109
  leftTopWidthHeight = [0, 0, 0, 0],
@@ -5879,88 +6159,17 @@ Niivue.prototype.draw3D = function (
5879
6159
  gl.enable(gl.DEPTH_TEST);
5880
6160
  gl.depthFunc(gl.ALWAYS);
5881
6161
  gl.clearDepth(0.0);
5882
-
5883
- if (this.volumes.length === 0) {
5884
- gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
5885
- this.drawMesh3D(true, 1, mvpMatrix, modelMatrix, normalMatrix);
5886
- gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5887
- return;
5888
- }
5889
- // mvp matrix and ray direction can now be a constant because of world space
5890
- const rayDir = this.calculateRayDirection(azimuth, elevation);
5891
-
5892
- let object3D = this.volumeObject3D;
5893
- // render picking surfaces
5894
- if (this.scene.mouseDepthPicker) {
5895
- //start PICKING: picking shader and reading values is slow
5896
- gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
5897
- this.scene.mouseDepthPicker = false;
5898
- if (object3D.isVisible && object3D.isPickable) {
5899
- let shader = this.pickingShader;
5900
- shader.use(this.gl);
5901
- gl.enable(gl.CULL_FACE);
5902
- gl.cullFace(gl.FRONT); //TH switch since we L/R flipped in calculateMvpMatrix
5903
- gl.uniformMatrix4fv(shader.mvpUniformLoc, false, mvpMatrix);
5904
- gl.uniform3fv(shader.rayDirUniformLoc, rayDir);
5905
- gl.uniform4fv(shader.clipPlaneUniformLoc, this.scene.clipPlane);
5906
- gl.uniform1i(shader.uniforms["id"], object3D.id);
5907
- gl.bindVertexArray(object3D.vao);
5908
- gl.drawElements(object3D.mode, object3D.indexCount, gl.UNSIGNED_SHORT, 0);
5909
- gl.bindVertexArray(this.unusedVAO);
5910
- }
5911
- //check if we have a selected object
5912
- const pixelX =
5913
- (this.mousePos[0] * leftTopWidthHeight[2]) / leftTopWidthHeight[2];
5914
- const pixelY =
5915
- gl.canvas.height -
5916
- (this.mousePos[1] * leftTopWidthHeight[3]) / leftTopWidthHeight[3] -
5917
- 1;
5918
- const rgbaPixel = new Uint8Array(4);
5919
- gl.readPixels(
5920
- pixelX, // x
5921
- pixelY, // y
5922
- 1, // width
5923
- 1, // height
5924
- gl.RGBA, // format
5925
- gl.UNSIGNED_BYTE, // type
5926
- rgbaPixel
5927
- ); // typed array to hold result
5928
-
5929
- this.selectedObjectId = rgbaPixel[3];
5930
- if (this.selectedObjectId === this.VOLUME_ID) {
5931
- this.scene.crosshairPos = new Float32Array(rgbaPixel.slice(0, 3)).map(
5932
- (x) => x / 255.0
5933
- );
5934
- }
5935
- //if (253 === rgbaPixel[3]) { ... clip plane clicked
5936
- } //end PICKING
5937
-
5938
- //gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
5939
- if (this.volumeObject3D.isVisible) {
5940
- gl.enable(gl.BLEND);
5941
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5942
- gl.enable(gl.CULL_FACE);
5943
- gl.cullFace(gl.FRONT); //TH switch since we L/R flipped in calculateMvpMatrix
5944
- let shader = this.renderShader; //.use(this.gl);
5945
- shader.use(this.gl);
5946
- gl.uniform1i(
5947
- shader.uniforms["backgroundMasksOverlays"],
5948
- this.backgroundMasksOverlays
5949
- );
5950
- gl.uniformMatrix4fv(shader.mvpUniformLoc, false, mvpMatrix);
5951
- gl.uniformMatrix4fv(shader.mvpMatRASLoc, false, this.back.matRAS);
5952
- gl.uniform3fv(shader.rayDirUniformLoc, rayDir);
5953
- gl.uniform4fv(shader.clipPlaneUniformLoc, this.scene.clipPlane);
5954
- gl.bindVertexArray(object3D.vao);
5955
- gl.drawElements(object3D.mode, object3D.indexCount, gl.UNSIGNED_SHORT, 0);
5956
- gl.bindVertexArray(this.unusedVAO);
5957
- }
6162
+ if (this.volumes.length > 0) this.drawImage3D(mvpMatrix, azimuth, elevation);
5958
6163
  if (!isMosaic) this.drawCrosshairs3D(true, 1.0, mvpMatrix);
5959
6164
  this.drawMesh3D(true, 1.0, mvpMatrix, modelMatrix, normalMatrix);
6165
+ if (this.scene.mouseDepthPicker) {
6166
+ this.depthPicker(leftTopWidthHeight, mvpMatrix, true);
6167
+ return;
6168
+ }
5960
6169
  this.drawMesh3D(false, 0.02, mvpMatrix, modelMatrix, normalMatrix);
5961
- // this.drawMesh3D(true, 1.0);
5962
6170
  if (!isMosaic) this.drawCrosshairs3D(false, 0.15, mvpMatrix);
5963
6171
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
6172
+ this.drawOrientationCube(leftTopWidthHeight, azimuth, elevation);
5964
6173
  let posString =
5965
6174
  "azimuth: " +
5966
6175
  this.scene.renderAzimuth.toFixed(0) +
@@ -6004,14 +6213,15 @@ Niivue.prototype.drawMesh3D = function (
6004
6213
  }
6005
6214
  gl.disable(gl.CULL_FACE); //show front and back faces
6006
6215
  //Draw the mesh
6007
- this.meshShader.use(this.gl); // set Shader
6216
+ let shader = this.meshShader;
6217
+ if (this.scene.mouseDepthPicker) shader = this.pickingMeshShader;
6218
+ shader.use(this.gl); // set Shader
6008
6219
  //set shader uniforms
6009
- gl.uniformMatrix4fv(this.meshShader.uniforms["mvpMtx"], false, m);
6010
- gl.uniformMatrix4fv(this.meshShader.uniforms["modelMtx"], false, modelMtx);
6011
- gl.uniformMatrix4fv(this.meshShader.uniforms["normMtx"], false, normMtx);
6012
- gl.uniform1f(this.meshShader.uniforms["opacity"], alpha);
6220
+ gl.uniformMatrix4fv(shader.uniforms["mvpMtx"], false, m);
6221
+ gl.uniformMatrix4fv(shader.uniforms["modelMtx"], false, modelMtx);
6222
+ gl.uniformMatrix4fv(shader.uniforms["normMtx"], false, normMtx);
6223
+ gl.uniform1f(shader.uniforms["opacity"], alpha);
6013
6224
  let hasFibers = false;
6014
-
6015
6225
  for (let i = 0; i < this.meshes.length; i++) {
6016
6226
  if (this.meshes[i].indexCount < 3) continue;
6017
6227
  if (this.meshes[i].offsetPt0) {
@@ -6033,7 +6243,7 @@ Niivue.prototype.drawMesh3D = function (
6033
6243
  gl.depthFunc(gl.ALWAYS);
6034
6244
  return;
6035
6245
  }
6036
- let shader = this.fiberShader;
6246
+ shader = this.fiberShader;
6037
6247
  shader.use(this.gl);
6038
6248
  gl.uniformMatrix4fv(shader.uniforms["mvpMtx"], false, m);
6039
6249
  gl.uniform1f(shader.uniforms["opacity"], alpha);
@@ -6074,18 +6284,22 @@ Niivue.prototype.drawCrosshairs3D = function (
6074
6284
  gl.deleteBuffer(this.crosshairs3D.indexBuffer); //TODO: handle in nvimage.js: create once, update with bufferSubData
6075
6285
  gl.deleteBuffer(this.crosshairs3D.vertexBuffer); //TODO: handle in nvimage.js: create once, update with bufferSubData
6076
6286
  }
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
- );
6287
+ let [mn, mx, range] = this.sceneExtentsMinMax();
6288
+ let radius = 1;
6289
+ if (this.volumes.length > 0)
6290
+ radius =
6291
+ 0.5 *
6292
+ Math.min(
6293
+ Math.min(this.back.pixDims[1], this.back.pixDims[2]),
6294
+ this.back.pixDims[3]
6295
+ );
6296
+ else if (range[0] < 50 || range[0] > 1000) radius = range[0] * 0.02; //2% of first dimension
6083
6297
  this.crosshairs3D = NiivueObject3D.generateCrosshairs(
6084
6298
  this.gl,
6085
6299
  1,
6086
6300
  mm,
6087
- this.volumeObject3D.extentsMin,
6088
- this.volumeObject3D.extentsMax,
6301
+ mn,
6302
+ mx,
6089
6303
  radius
6090
6304
  );
6091
6305
  //this.crosshairs3D.minExtent = this.volumeObject3D.minExtent;
@@ -6117,7 +6331,6 @@ Niivue.prototype.drawCrosshairs3D = function (
6117
6331
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
6118
6332
  gl.depthFunc(gl.ALWAYS);
6119
6333
  }
6120
-
6121
6334
  color[3] = alpha;
6122
6335
  gl.uniform4fv(crosshairsShader.uniforms["surfaceColor"], color);
6123
6336
  gl.bindVertexArray(this.crosshairs3D.vao);
@@ -6133,6 +6346,14 @@ Niivue.prototype.drawCrosshairs3D = function (
6133
6346
  // not included in public docs
6134
6347
  Niivue.prototype.mm2frac = function (mm, volIdx = 0) {
6135
6348
  //given mm, return volume fraction
6349
+ if (this.volumes.length < 1) {
6350
+ let frac = [0.1, 0.5, 0.5];
6351
+ let [mn, mx, range] = this.sceneExtentsMinMax();
6352
+ frac[0] = (mm[0] - mn[0]) / range[0];
6353
+ frac[1] = (mm[1] - mn[1]) / range[1];
6354
+ frac[2] = (mm[2] - mn[2]) / range[2];
6355
+ return frac;
6356
+ }
6136
6357
  //convert from object space in millimeters to normalized texture space XYZ= [0..1, 0..1 ,0..1]
6137
6358
  let mm4 = mat.vec4.fromValues(mm[0], mm[1], mm[2], 1);
6138
6359
  let d = this.volumes[volIdx].dimsRAS;
@@ -6187,7 +6408,15 @@ Niivue.prototype.moveCrosshairInVox = function (x, y, z) {
6187
6408
 
6188
6409
  Niivue.prototype.frac2mm = function (frac, volIdx = 0) {
6189
6410
  let pos = mat.vec4.fromValues(frac[0], frac[1], frac[2], 1);
6190
- mat.vec4.transformMat4(pos, pos, this.volumes[volIdx].frac2mm);
6411
+ if (this.volumes.length > 0)
6412
+ mat.vec4.transformMat4(pos, pos, this.volumes[volIdx].frac2mm);
6413
+ else {
6414
+ let [mn, mx] = this.sceneExtentsMinMax();
6415
+ const lerp = (x, y, a) => x * (1 - a) + y * a;
6416
+ pos[0] = lerp(mn[0], mx[0], frac[0]);
6417
+ pos[1] = lerp(mn[1], mx[1], frac[1]);
6418
+ pos[2] = lerp(mn[2], mx[2], frac[2]);
6419
+ }
6191
6420
  return pos;
6192
6421
  };
6193
6422
 
@@ -6261,20 +6490,25 @@ Niivue.prototype.canvasPos2frac = function (canvasPos) {
6261
6490
 
6262
6491
  // not included in public docs
6263
6492
  // note: we also have a "sliceScale" method, which could be confusing
6264
- Niivue.prototype.scaleSlice = function (w, h) {
6265
- let scalePix = this.gl.canvas.clientWidth / w;
6266
- if (h * scalePix > this.effectiveCanvasHeight())
6267
- scalePix = this.effectiveCanvasHeight() / h;
6493
+ Niivue.prototype.scaleSlice = function (
6494
+ w,
6495
+ h,
6496
+ widthPadPixels = 0,
6497
+ heightPadPixels = 0
6498
+ ) {
6499
+ let canvasW = this.canvas.width - widthPadPixels;
6500
+ let canvasH = this.effectiveCanvasHeight() - heightPadPixels;
6501
+ let scalePix = canvasW / w;
6502
+ if (h * scalePix > canvasH) scalePix = canvasH / h;
6268
6503
  //canvas space is 0,0...w,h with origin at upper left
6269
6504
  let wPix = w * scalePix;
6270
6505
  let hPix = h * scalePix;
6271
6506
  let leftTopWidthHeight = [
6272
- (this.gl.canvas.clientWidth - wPix) * 0.5,
6273
- (this.effectiveCanvasHeight() - hPix) * 0.5,
6507
+ (canvasW - wPix) * 0.5,
6508
+ (canvasH - hPix) * 0.5,
6274
6509
  wPix,
6275
6510
  hPix,
6276
6511
  ];
6277
- //let leftTopWidthHeight = [(gl.canvas.clientWidth-wPix) * 0.5, 80, wPix, hPix];
6278
6512
  return leftTopWidthHeight;
6279
6513
  }; // scaleSlice()
6280
6514
 
@@ -6663,7 +6897,8 @@ Niivue.prototype.drawScene = function () {
6663
6897
  this.opts.backColor[2],
6664
6898
  this.opts.backColor[3]
6665
6899
  );
6666
- this.gl.clear(this.gl.COLOR_BUFFER_BIT);
6900
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
6901
+ //this.gl.clear(this.gl.COLOR_BUFFER_BIT);
6667
6902
  if (this.bmpTexture) {
6668
6903
  //draw the thumbnail image and exit
6669
6904
  this.drawThumbnail();
@@ -6696,9 +6931,6 @@ Niivue.prototype.drawScene = function () {
6696
6931
  depthAziElev[1] -= x;
6697
6932
  depthAziElev[1] = depthAziElev[1] % 360;
6698
6933
  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
6934
  if (
6703
6935
  depthAziElev[1] !== this.scene.clipPlaneDepthAziElev[1] ||
6704
6936
  depthAziElev[2] !== this.scene.clipPlaneDepthAziElev[2]
@@ -6732,19 +6964,28 @@ Niivue.prototype.drawScene = function () {
6732
6964
  } else {
6733
6965
  //sliceTypeMultiplanar
6734
6966
  let { volScale, vox, longestAxis } = this.sliceScale();
6967
+ //scaleSlice(w, h, widthPadPixels, heightPadPixels) {
6968
+ let pad = this.opts.multiplanarPadPixels;
6969
+ // size for 2 rows, 2 columns
6735
6970
  let ltwh = this.scaleSlice(
6736
6971
  volScale[0] + volScale[1],
6737
- volScale[1] + volScale[2]
6972
+ volScale[1] + volScale[2],
6973
+ pad * 1,
6974
+ pad * 1
6738
6975
  );
6739
6976
  let wX = (ltwh[2] * volScale[0]) / (volScale[0] + volScale[1]);
6977
+ // size for 1 row, 3 columns
6740
6978
  let ltwh3x1 = this.scaleSlice(
6741
6979
  volScale[0] + volScale[0] + volScale[1],
6742
- Math.max(volScale[1], volScale[2])
6980
+ Math.max(volScale[1], volScale[2]),
6981
+ pad * 2
6743
6982
  );
6744
6983
  let mx = Math.max(Math.max(volScale[1], volScale[2]), volScale[0]);
6984
+ // size for 1 row, 4 columns
6745
6985
  let ltwh4x1 = this.scaleSlice(
6746
6986
  volScale[0] + volScale[0] + volScale[1] + mx,
6747
- mx
6987
+ mx,
6988
+ pad * 3
6748
6989
  );
6749
6990
  let wX1 =
6750
6991
  (ltwh3x1[2] * volScale[0]) / (volScale[0] + volScale[0] + volScale[1]);
@@ -6756,7 +6997,7 @@ Niivue.prototype.drawScene = function () {
6756
6997
  if (ltwh3x1[3] === ltwh4x1[3]) {
6757
6998
  ltwh3x1 = ltwh4x1;
6758
6999
  this.draw3D([
6759
- ltwh3x1[0] + wX1 + wX1 + hY1,
7000
+ ltwh3x1[0] + wX1 + wX1 + hY1 + pad * 3,
6760
7001
  ltwh3x1[1],
6761
7002
  ltwh4x1[3],
6762
7003
  ltwh4x1[3],
@@ -6765,21 +7006,24 @@ Niivue.prototype.drawScene = function () {
6765
7006
  //draw axial
6766
7007
  this.draw2D([ltwh3x1[0], ltwh3x1[1], wX1, hY1], 0);
6767
7008
  //draw coronal
6768
- this.draw2D([ltwh3x1[0] + wX1, ltwh3x1[1], wX1, hZ1], 1);
7009
+ this.draw2D([ltwh3x1[0] + wX1 + pad, ltwh3x1[1], wX1, hZ1], 1);
6769
7010
  //draw sagittal
6770
- this.draw2D([ltwh3x1[0] + wX1 + wX1, ltwh3x1[1], hY1, hZ1], 2);
7011
+ this.draw2D(
7012
+ [ltwh3x1[0] + wX1 + wX1 + pad * 2, ltwh3x1[1], hY1, hZ1],
7013
+ 2
7014
+ );
6771
7015
  } else {
6772
7016
  let wY = ltwh[2] - wX;
6773
7017
  let hY = (ltwh[3] * volScale[1]) / (volScale[1] + volScale[2]);
6774
7018
  let hZ = ltwh[3] - hY;
6775
7019
  //draw axial
6776
- this.draw2D([ltwh[0], ltwh[1] + hZ, wX, hY], 0);
7020
+ this.draw2D([ltwh[0], ltwh[1] + hZ + pad, wX, hY], 0);
6777
7021
  //draw coronal
6778
7022
  this.draw2D([ltwh[0], ltwh[1], wX, hZ], 1);
6779
7023
  //draw sagittal
6780
- this.draw2D([ltwh[0] + wX, ltwh[1], wY, hZ], 2);
7024
+ this.draw2D([ltwh[0] + wX + pad, ltwh[1], wY, hZ], 2);
6781
7025
  if (!this.graph.autoSizeMultiplanar)
6782
- this.draw3D([ltwh[0] + wX, ltwh[1] + hZ, wY, hY]);
7026
+ this.draw3D([ltwh[0] + wX + pad, ltwh[1] + hZ + pad, wY, hY]);
6783
7027
  } //if landscape else portrait
6784
7028
  } //if multiplanar
6785
7029
  } //if mosaic not 2D