@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/README.md +15 -17
- package/dist/niivue.es.js +2796 -520
- package/dist/niivue.umd.js +303 -142
- package/package.json +1 -1
- package/src/colortables.js +1 -1
- package/src/index.html +3 -2
- package/src/indexDraw.html +7 -2
- package/src/indexMM.html +229 -0
- package/src/indexModulate.html +223 -0
- package/src/niivue.js +1657 -575
- package/src/nvimage.js +125 -26
- package/src/shader-srcs.js +209 -59
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
}; //
|
|
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
|
-
}; //
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
3279
|
-
this.
|
|
3280
|
-
|
|
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
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
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
|
-
|
|
4018
|
-
|
|
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
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4070
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4209
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
4232
|
-
|
|
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
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
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.
|
|
4669
|
+
this.gl.uniform4fv(
|
|
4261
4670
|
this.colorbarShader.uniforms["leftTopWidthHeight"],
|
|
4262
|
-
|
|
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
|
-
|
|
4281
|
-
|
|
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
|
-
}; //
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
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
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
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
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
mat.mat4.
|
|
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
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
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
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
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
|
-
|
|
4480
|
-
|
|
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
|
-
|
|
4483
|
-
|
|
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 (
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
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
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
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
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
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
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
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
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
gl.
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
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
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
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
|
-
|
|
5217
|
+
this.readyForSync = true;
|
|
5218
|
+
}; // draw2DMM()
|
|
4544
5219
|
|
|
4545
5220
|
// not included in public docs
|
|
4546
|
-
Niivue.prototype.
|
|
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
|
-
|
|
4552
|
-
|
|
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
|
-
|
|
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
|
|
4613
|
-
leftTopWidthHeight
|
|
4614
|
-
|
|
4615
|
-
|
|
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.
|
|
5311
|
+
this.gl.bindVertexArray(this.genericVAO);
|
|
5312
|
+
this.rectShader.use(this.gl);
|
|
4618
5313
|
this.gl.uniform4fv(
|
|
4619
|
-
this.
|
|
5314
|
+
this.rectShader.uniforms["lineColor"],
|
|
4620
5315
|
this.opts.crosshairColor
|
|
4621
5316
|
);
|
|
4622
|
-
this.gl.uniform2fv(this.
|
|
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.
|
|
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.
|
|
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
|
-
}; //
|
|
5348
|
+
}; // draw2DVox()
|
|
4696
5349
|
|
|
4697
|
-
Niivue.prototype.
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
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.
|
|
4915
|
-
this.gl.uniform4fv(this.
|
|
4916
|
-
this.gl.uniform2fv(this.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5205
|
-
|
|
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 =
|
|
5912
|
+
const pixelX =
|
|
5913
|
+
(this.mousePos[0] * leftTopWidthHeight[2]) / leftTopWidthHeight[2];
|
|
5231
5914
|
const pixelY =
|
|
5232
5915
|
gl.canvas.height -
|
|
5233
|
-
(this.mousePos[1] *
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
5380
|
-
|
|
5381
|
-
|
|
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
|
|
6098
|
+
let modelMtx, normMtx;
|
|
5399
6099
|
// eslint-disable-next-line no-unused-vars
|
|
5400
|
-
|
|
5401
|
-
|
|
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
|
-
|
|
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
|
-
};
|
|
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
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
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
|
-
|
|
5521
|
-
|
|
5522
|
-
fracX = 1.0 - fracX;
|
|
6241
|
+
if (axCorSag === this.sliceTypeCoronal) {
|
|
6242
|
+
texFrac[0] = fracX;
|
|
6243
|
+
texFrac[2] = fracY;
|
|
5523
6244
|
}
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
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
|
-
|
|
5557
|
-
|
|
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.
|
|
5564
|
-
scalePix = this.
|
|
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.
|
|
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
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5652
|
-
this.
|
|
5653
|
-
|
|
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
|
-
//
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
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
|
-
|
|
5692
|
-
let
|
|
5693
|
-
let
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
]);
|
|
5709
|
-
|
|
5710
|
-
|
|
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()
|