@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/dist/niivue.es.js +111 -26
- package/dist/niivue.umd.js +35 -35
- package/package.json +2 -2
- package/src/indexMM.html +1 -1
- package/src/indexOrient.html +215 -0
- package/src/niivue.js +410 -166
- package/src/orientCube.js +64 -0
- package/src/shader-srcs.js +52 -41
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.
|
|
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
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
3331
|
-
this.gl.uniform1i(this.
|
|
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.
|
|
3334
|
-
this.
|
|
3335
|
-
|
|
3336
|
-
this.
|
|
3337
|
-
this.
|
|
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
|
|
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.
|
|
4149
|
+
this.pickingImageShader.use(this.gl);
|
|
4035
4150
|
this.gl.uniform1f(
|
|
4036
|
-
this.
|
|
4151
|
+
this.pickingImageShader.uniforms["overlays"],
|
|
4037
4152
|
this.overlays.length
|
|
4038
4153
|
);
|
|
4039
|
-
this.gl.uniform3fv(this.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
6010
|
-
gl.uniformMatrix4fv(
|
|
6011
|
-
gl.uniformMatrix4fv(
|
|
6012
|
-
gl.uniform1f(
|
|
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
|
-
|
|
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
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
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
|
-
|
|
6088
|
-
|
|
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
|
-
|
|
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 (
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
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
|
-
(
|
|
6273
|
-
(
|
|
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(
|
|
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
|