@niivue/niivue 0.24.2 → 0.26.3
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 +2496 -540
- package/dist/niivue.umd.js +295 -125
- package/package.json +1 -1
- package/src/colortables.js +2 -2
- package/src/index.html +3 -2
- package/src/indexDraw.html +11 -4
- package/src/indexMM.html +229 -0
- package/src/indexModulate.html +223 -0
- package/src/niivue.js +2110 -616
- package/src/nvimage.js +132 -30
- package/src/nvmesh.js +33 -14
- package/src/shader-srcs.js +248 -67
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,
|
|
@@ -51,6 +58,9 @@ import { Log } from "./logger";
|
|
|
51
58
|
import defaultFontPNG from "./fonts/Roboto-Regular.png";
|
|
52
59
|
import defaultFontMetrics from "./fonts/Roboto-Regular.json";
|
|
53
60
|
import { colortables } from "./colortables";
|
|
61
|
+
import { webSocket } from "rxjs/webSocket";
|
|
62
|
+
import { interval } from "rxjs";
|
|
63
|
+
|
|
54
64
|
const log = new Log();
|
|
55
65
|
const cmapper = new colortables();
|
|
56
66
|
|
|
@@ -91,11 +101,13 @@ export function Niivue(options = {}) {
|
|
|
91
101
|
textHeight: 0.06, // 0 for no text, fraction of canvas min(height,width)
|
|
92
102
|
colorbarHeight: 0.05, // 0 for no colorbars, fraction of Nifti j dimension
|
|
93
103
|
crosshairWidth: 1, // 0 for no crosshairs
|
|
104
|
+
rulerWidth: 4,
|
|
94
105
|
show3Dcrosshair: false,
|
|
95
106
|
backColor: [0, 0, 0, 1],
|
|
96
107
|
crosshairColor: [1, 0, 0, 1],
|
|
97
108
|
selectionBoxColor: [1, 1, 1, 0.5],
|
|
98
109
|
clipPlaneColor: [0.7, 0, 0.7, 0.5],
|
|
110
|
+
rulerColor: [1, 0, 0, 0.8],
|
|
99
111
|
colorBarMargin: 0.05, // x axis margin arount color bar, clip space coordinates
|
|
100
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.
|
|
101
113
|
clipPlaneHotKey: "KeyC", // keyboard short cut to activate the clip plane
|
|
@@ -103,7 +115,14 @@ export function Niivue(options = {}) {
|
|
|
103
115
|
keyDebounceTime: 50, // default debounce time used in keyup listeners
|
|
104
116
|
isNearestInterpolation: false,
|
|
105
117
|
isAtlasOutline: false,
|
|
118
|
+
isRuler: false,
|
|
119
|
+
isColorbar: false,
|
|
106
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,
|
|
107
126
|
logging: false,
|
|
108
127
|
loadingText: "waiting for images...",
|
|
109
128
|
dragAndDropEnabled: true,
|
|
@@ -111,6 +130,9 @@ export function Niivue(options = {}) {
|
|
|
111
130
|
penValue: 1, // sets drawing color. see "drawPt"
|
|
112
131
|
isFilledPen: false,
|
|
113
132
|
thumbnail: "",
|
|
133
|
+
onLocationChange: () => {},
|
|
134
|
+
onIntensityChange: () => {},
|
|
135
|
+
onImageLoaded: () => {},
|
|
114
136
|
};
|
|
115
137
|
|
|
116
138
|
this.canvas = null; // the canvas element on the page
|
|
@@ -119,8 +141,11 @@ export function Niivue(options = {}) {
|
|
|
119
141
|
this.volumeTexture = null;
|
|
120
142
|
this.drawTexture = null; //the GPU memory storage of the drawing
|
|
121
143
|
this.drawBitmap = null; //the CPU memory storage of the drawing
|
|
122
|
-
this.
|
|
144
|
+
this.maxDrawUndoBitmaps = 8; //analogy: number of bullets in revolver, we will cycle these
|
|
145
|
+
this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //analogy: cylinder position of a revolver
|
|
146
|
+
this.drawUndoBitmaps = [];
|
|
123
147
|
this.drawOpacity = 0.8;
|
|
148
|
+
this.colorbarHeight = 0; //height in pixels, set when colorbar is drawn
|
|
124
149
|
this.drawPenLocation = [NaN, NaN, NaN];
|
|
125
150
|
this.drawPenAxCorSag = -1; //do not allow pen to drag between Sagittal/Coronal/Axial
|
|
126
151
|
this.drawFillOverwrites = true;
|
|
@@ -128,7 +153,8 @@ export function Niivue(options = {}) {
|
|
|
128
153
|
this.overlayTexture = null;
|
|
129
154
|
this.overlayTextureID = [];
|
|
130
155
|
this.sliceShader = null;
|
|
131
|
-
this.
|
|
156
|
+
this.sliceMMShader = null;
|
|
157
|
+
this.rectShader = null;
|
|
132
158
|
this.graphShader = null;
|
|
133
159
|
this.renderShader = null;
|
|
134
160
|
this.pickingShader = null;
|
|
@@ -162,6 +188,8 @@ export function Niivue(options = {}) {
|
|
|
162
188
|
this.sliceTypeSagittal = 2;
|
|
163
189
|
this.sliceTypeMultiplanar = 3;
|
|
164
190
|
this.sliceTypeRender = 4;
|
|
191
|
+
//this.sliceTypeMosaic = 5; //kava
|
|
192
|
+
this.sliceMosaicString = "";
|
|
165
193
|
this.sliceType = this.sliceTypeMultiplanar; // sets current view in webgl canvas
|
|
166
194
|
this.scene = {};
|
|
167
195
|
this.syncOpts = {};
|
|
@@ -178,6 +206,7 @@ export function Niivue(options = {}) {
|
|
|
178
206
|
this.scene.mouseButtonLeftDown = false;
|
|
179
207
|
this.scene.mouseButtonRightDown = false;
|
|
180
208
|
this.scene.mouseDepthPicker = false;
|
|
209
|
+
this.scene.pan2Dxyzmm = [0, 0, 0, 1];
|
|
181
210
|
this.scene.scale2D = 1.0;
|
|
182
211
|
this.scene.prevX = 0;
|
|
183
212
|
this.scene.prevY = 0;
|
|
@@ -194,14 +223,7 @@ export function Niivue(options = {}) {
|
|
|
194
223
|
this.volScale = [];
|
|
195
224
|
this.vox = [];
|
|
196
225
|
this.mousePos = [0, 0];
|
|
197
|
-
this.
|
|
198
|
-
this.screenSlices = [
|
|
199
|
-
//location and type of each 2D slice on screen, allows clicking to detect position
|
|
200
|
-
{ leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
|
|
201
|
-
{ leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
|
|
202
|
-
{ leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
|
|
203
|
-
{ leftTopWidthHeight: [1, 0, 0, 1], axCorSag: this.sliceTypeAxial },
|
|
204
|
-
];
|
|
226
|
+
this.screenSlices = []; // empty array
|
|
205
227
|
this.isDragging = false;
|
|
206
228
|
this.dragStart = [0.0, 0.0];
|
|
207
229
|
this.dragEnd = [0.0, 0.0];
|
|
@@ -211,10 +233,7 @@ export function Niivue(options = {}) {
|
|
|
211
233
|
this.volumeObject3D = null;
|
|
212
234
|
this.pivot3D = [0, 0, 0]; //center for rendering rotation
|
|
213
235
|
this.furthestFromPivot = 10.0; //most distant point from pivot
|
|
214
|
-
this.intensityRange$ = new Subject(); // an array
|
|
215
|
-
this.scene.location$ = new Subject(); // object with properties: {mm: [N N N], vox: [N N N], frac: [N N N]}
|
|
216
236
|
this.scene.loading$ = new Subject(); // whether or not the scene is loading
|
|
217
|
-
this.imageLoaded$ = new Subject();
|
|
218
237
|
this.currentClipPlaneIndex = 0;
|
|
219
238
|
this.lastCalled = new Date().getTime();
|
|
220
239
|
this.multiTouchGesture = false;
|
|
@@ -255,8 +274,20 @@ export function Niivue(options = {}) {
|
|
|
255
274
|
Name: "Toon",
|
|
256
275
|
Frag: fragMeshToonShader,
|
|
257
276
|
},
|
|
277
|
+
{
|
|
278
|
+
Name: "Depth",
|
|
279
|
+
Frag: fragMeshDepthShader,
|
|
280
|
+
},
|
|
258
281
|
];
|
|
259
282
|
|
|
283
|
+
// multiuser
|
|
284
|
+
this.isController = false;
|
|
285
|
+
this.isInSession = false;
|
|
286
|
+
this.sessionKey = "";
|
|
287
|
+
this.sessionUrl = "";
|
|
288
|
+
this.serverConnection$ = null;
|
|
289
|
+
this.interval$ = null;
|
|
290
|
+
|
|
260
291
|
this.initialized = false;
|
|
261
292
|
// loop through known Niivue properties
|
|
262
293
|
// if the user supplied opts object has a
|
|
@@ -280,16 +311,37 @@ export function Niivue(options = {}) {
|
|
|
280
311
|
|
|
281
312
|
// maping of keys (event strings) to rxjs subjects
|
|
282
313
|
this.eventsToSubjects = {
|
|
283
|
-
location: this.scene.location$,
|
|
284
314
|
loading: this.scene.loading$,
|
|
285
|
-
imageLoaded: this.imageLoaded$,
|
|
286
|
-
intensityRange: this.intensityRange$,
|
|
287
315
|
};
|
|
288
316
|
|
|
289
317
|
// rxjs subscriptions. Keeping a reference array like this allows us to unsubscribe later
|
|
290
318
|
this.subscriptions = [];
|
|
291
319
|
}
|
|
292
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
|
+
|
|
293
345
|
/**
|
|
294
346
|
* attach the Niivue instance to the webgl2 canvas by element id
|
|
295
347
|
* @param {string} id the id of an html canvas element
|
|
@@ -320,9 +372,6 @@ Niivue.prototype.attachTo = async function (id) {
|
|
|
320
372
|
* // data has the shape {mm: [N, N, N], vox: [N, N, N], frac: [N, N, N], values: this.volumes.map(v => {return val})}
|
|
321
373
|
* //...
|
|
322
374
|
* }
|
|
323
|
-
* niivue.on('location', doSomethingWithLocationData)
|
|
324
|
-
* niivue.on('intensityRange', callback)
|
|
325
|
-
* niivue.on('imageLoaded', callback)
|
|
326
375
|
*/
|
|
327
376
|
Niivue.prototype.on = function (event, callback) {
|
|
328
377
|
let knownEvents = Object.keys(this.eventsToSubjects);
|
|
@@ -434,6 +483,142 @@ Niivue.prototype.sync = function () {
|
|
|
434
483
|
this.otherNV.drawScene();
|
|
435
484
|
};
|
|
436
485
|
|
|
486
|
+
// Internal function to connect to web socket server
|
|
487
|
+
Niivue.prototype.connectToServer = function (wsServerUrl, sessionName) {
|
|
488
|
+
const url = new URL(wsServerUrl);
|
|
489
|
+
url.pathname = "websockets";
|
|
490
|
+
url.search = "?session=" + sessionName;
|
|
491
|
+
this.serverConnection$ = webSocket(url.href);
|
|
492
|
+
console.log(url.href);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Internal function to schedule updates to the server
|
|
496
|
+
Niivue.prototype.setUpdateInterval = function () {
|
|
497
|
+
this.interval$ = interval(300);
|
|
498
|
+
this.interval$.subscribe(() => {
|
|
499
|
+
this.serverConnection$.next({
|
|
500
|
+
op: "update",
|
|
501
|
+
azimuth: this.scene.renderAzimuth,
|
|
502
|
+
elevation: this.scene.renderElevation,
|
|
503
|
+
clipPlane: this.scene.clipPlane,
|
|
504
|
+
zoom: this.volScaleMultiplier,
|
|
505
|
+
key: this.sessionKey,
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Internal function called after a connection with the server has been made
|
|
511
|
+
Niivue.prototype.subscribeToServer = function (
|
|
512
|
+
sessionCreatedCallback,
|
|
513
|
+
sessionJoinedCallback
|
|
514
|
+
) {
|
|
515
|
+
this.serverConnection$.subscribe({
|
|
516
|
+
next: (msg) => {
|
|
517
|
+
switch (msg["op"]) {
|
|
518
|
+
case "update":
|
|
519
|
+
this.scene.renderAzimuth = msg["azimuth"];
|
|
520
|
+
this.scene.renderElevation = msg["elevation"];
|
|
521
|
+
this.volScaleMultiplier = msg["zoom"];
|
|
522
|
+
this.scene.clipPlane = msg["clipPlane"];
|
|
523
|
+
this.drawScene();
|
|
524
|
+
break;
|
|
525
|
+
|
|
526
|
+
case "create":
|
|
527
|
+
console.log(msg);
|
|
528
|
+
if (!msg["isError"]) {
|
|
529
|
+
this.isInSession = true;
|
|
530
|
+
this.sessionKey = msg["key"];
|
|
531
|
+
this.setUpdateInterval();
|
|
532
|
+
}
|
|
533
|
+
if (sessionCreatedCallback) {
|
|
534
|
+
sessionCreatedCallback(
|
|
535
|
+
msg["message"],
|
|
536
|
+
msg["url"],
|
|
537
|
+
msg["key"],
|
|
538
|
+
msg["isError"]
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
|
|
543
|
+
case "join":
|
|
544
|
+
this.isInSession = true;
|
|
545
|
+
this.isController = msg["isController"];
|
|
546
|
+
if (this.isController) {
|
|
547
|
+
this.setUpdateInterval();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (sessionJoinedCallback) {
|
|
551
|
+
sessionJoinedCallback(
|
|
552
|
+
msg["message"],
|
|
553
|
+
msg["url"],
|
|
554
|
+
msg["isController"]
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}, // Called whenever there is a message from the server.
|
|
560
|
+
error: (err) => console.log(err), // Called if at any point WebSocket API signals some kind of error.
|
|
561
|
+
complete: () => console.log("complete"), // Called when connection is closed (for whatever reason).
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create a multiuser session
|
|
567
|
+
* @param {string} wsServerUrl e.g. ws://localhost:3000
|
|
568
|
+
* @param {string} sessionName
|
|
569
|
+
* @param {function(string, string, string, boolean):void} sessionCreatedCallback callback after session has been created with message, session url, session key
|
|
570
|
+
* if there was no error.
|
|
571
|
+
*/
|
|
572
|
+
Niivue.prototype.createSession = function (
|
|
573
|
+
wsServerUrl,
|
|
574
|
+
sessionName,
|
|
575
|
+
sessionCreatedCallback
|
|
576
|
+
) {
|
|
577
|
+
this.connectToServer(wsServerUrl, sessionName);
|
|
578
|
+
|
|
579
|
+
// subscribe to any messages from the server
|
|
580
|
+
this.subscribeToServer(sessionCreatedCallback);
|
|
581
|
+
|
|
582
|
+
// tell the server we want to create a sesion
|
|
583
|
+
this.serverConnection$.next({
|
|
584
|
+
op: "create",
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Join a multiuser session
|
|
590
|
+
* @param {string} wsServerUrl e.g. ws://localhost:3000
|
|
591
|
+
* @param {string} sessionName
|
|
592
|
+
* @param {function(string, string, boolean):void} sessionJoinedCallback callback after session has been joined with message, session url and session key
|
|
593
|
+
*/
|
|
594
|
+
Niivue.prototype.joinSession = function (
|
|
595
|
+
wsServerUrl,
|
|
596
|
+
sessionName,
|
|
597
|
+
key,
|
|
598
|
+
sessionJoinedCallback
|
|
599
|
+
) {
|
|
600
|
+
this.connectToServer(wsServerUrl, sessionName);
|
|
601
|
+
|
|
602
|
+
// subscribe to any messages from the server
|
|
603
|
+
this.subscribeToServer(null, sessionJoinedCallback);
|
|
604
|
+
|
|
605
|
+
// tell the server we want to create a sesion
|
|
606
|
+
this.serverConnection$.next({
|
|
607
|
+
op: "join",
|
|
608
|
+
key: key,
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Close a multiuser session
|
|
614
|
+
*/
|
|
615
|
+
Niivue.prototype.closeSession = function () {
|
|
616
|
+
this.interval$.complete();
|
|
617
|
+
this.serverConnection$.complete();
|
|
618
|
+
this.isInSession = false;
|
|
619
|
+
this.isController = false;
|
|
620
|
+
};
|
|
621
|
+
|
|
437
622
|
/* Not documented publicly for now
|
|
438
623
|
* test if two arrays have equal values for each element
|
|
439
624
|
* @param {Array} a the first array
|
|
@@ -565,7 +750,10 @@ function intensityRaw2Scaled(hdr, raw) {
|
|
|
565
750
|
// not included in public docs
|
|
566
751
|
// note: no test yet
|
|
567
752
|
Niivue.prototype.calculateNewRange = function (volIdx = 0) {
|
|
568
|
-
if (
|
|
753
|
+
if (
|
|
754
|
+
this.sliceType === this.sliceTypeRender &&
|
|
755
|
+
this.sliceMosaicString.length < 1
|
|
756
|
+
) {
|
|
569
757
|
return;
|
|
570
758
|
}
|
|
571
759
|
if (
|
|
@@ -575,9 +763,10 @@ Niivue.prototype.calculateNewRange = function (volIdx = 0) {
|
|
|
575
763
|
return;
|
|
576
764
|
// calculate our box
|
|
577
765
|
let frac = this.canvasPos2frac([this.dragStart[0], this.dragStart[1]]);
|
|
766
|
+
if (frac[0] < 0) return;
|
|
578
767
|
let startVox = this.frac2vox(frac, volIdx);
|
|
579
|
-
|
|
580
768
|
frac = this.canvasPos2frac([this.dragEnd[0], this.dragEnd[1]]);
|
|
769
|
+
if (frac[0] < 0) return;
|
|
581
770
|
let endVox = this.frac2vox(frac, volIdx);
|
|
582
771
|
|
|
583
772
|
let hi = -Number.MAX_VALUE,
|
|
@@ -624,7 +813,7 @@ Niivue.prototype.calculateNewRange = function (volIdx = 0) {
|
|
|
624
813
|
var mxScale = intensityRaw2Scaled(hdr, hi);
|
|
625
814
|
this.volumes[volIdx].cal_min = mnScale;
|
|
626
815
|
this.volumes[volIdx].cal_max = mxScale;
|
|
627
|
-
this.
|
|
816
|
+
this.opts.onIntensityChange(this.volumes[volIdx]);
|
|
628
817
|
};
|
|
629
818
|
|
|
630
819
|
// not included in public docs
|
|
@@ -635,10 +824,13 @@ Niivue.prototype.mouseUpListener = function () {
|
|
|
635
824
|
this.scene.mouseButtonRightDown = false;
|
|
636
825
|
this.scene.mouseButtonLeftDown = false;
|
|
637
826
|
if (this.drawPenFillPts.length > 0) this.drawPenFilled();
|
|
827
|
+
else if (this.drawPenAxCorSag >= 0) this.drawAddUndoBitmap();
|
|
638
828
|
this.drawPenLocation = [NaN, NaN, NaN];
|
|
639
829
|
this.drawPenAxCorSag = -1;
|
|
830
|
+
|
|
640
831
|
if (this.isDragging) {
|
|
641
832
|
this.isDragging = false;
|
|
833
|
+
if (this.opts.isDragShowsMeasurementTool) return;
|
|
642
834
|
this.calculateNewRange();
|
|
643
835
|
this.refreshLayers(this.volumes[0], 0, this.volumes.length);
|
|
644
836
|
}
|
|
@@ -711,19 +903,28 @@ Niivue.prototype.mouseMoveListener = function (e) {
|
|
|
711
903
|
// note: should update this to accept a volume index to reset a selected volume rather than only the background (TODO)
|
|
712
904
|
// reset brightness and contrast to global min and max
|
|
713
905
|
// note: no test yet
|
|
714
|
-
Niivue.prototype.resetBriCon = function () {
|
|
906
|
+
Niivue.prototype.resetBriCon = function (msg = null) {
|
|
715
907
|
//this.volumes[0].cal_min = this.volumes[0].global_min;
|
|
716
908
|
//this.volumes[0].cal_max = this.volumes[0].global_max;
|
|
717
909
|
|
|
718
910
|
// don't reset bri/con if the user is in 3D mode and double clicks
|
|
911
|
+
let isRender = false;
|
|
719
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) {
|
|
720
920
|
this.scene.mouseDepthPicker = true;
|
|
721
921
|
this.drawScene();
|
|
922
|
+
this.drawScene();
|
|
722
923
|
return;
|
|
723
924
|
}
|
|
724
925
|
this.volumes[0].cal_min = this.volumes[0].robust_min;
|
|
725
926
|
this.volumes[0].cal_max = this.volumes[0].robust_max;
|
|
726
|
-
this.
|
|
927
|
+
this.opts.onIntensityChange(this.volumes[0]);
|
|
727
928
|
this.refreshLayers(this.volumes[0], 0, this.volumes.length);
|
|
728
929
|
this.drawScene();
|
|
729
930
|
};
|
|
@@ -787,9 +988,9 @@ Niivue.prototype.handlePinchZoom = function (e) {
|
|
|
787
988
|
// handler for keyboard shortcuts
|
|
788
989
|
Niivue.prototype.keyUpListener = function (e) {
|
|
789
990
|
if (e.code === this.opts.clipPlaneHotKey) {
|
|
790
|
-
if (this.sliceType != this.sliceTypeRender) {
|
|
991
|
+
/*if (this.sliceType != this.sliceTypeRender) {
|
|
791
992
|
return;
|
|
792
|
-
}
|
|
993
|
+
}*/ //bravo
|
|
793
994
|
let now = new Date().getTime();
|
|
794
995
|
let elapsed = now - this.lastCalled;
|
|
795
996
|
if (elapsed > this.opts.keyDebounceTime) {
|
|
@@ -1140,10 +1341,32 @@ Niivue.prototype.dropListener = async function (e) {
|
|
|
1140
1341
|
this.drawScene(); //<- this seems to be required if you drag and drop a mesh, not a volume
|
|
1141
1342
|
};
|
|
1142
1343
|
|
|
1344
|
+
Niivue.prototype.setCornerOrientationText = function (isCornerOrientationText) {
|
|
1345
|
+
this.opts.isCornerOrientationText = isCornerOrientationText;
|
|
1346
|
+
this.updateGLVolume();
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1143
1349
|
Niivue.prototype.setRadiologicalConvention = function (
|
|
1144
1350
|
isRadiologicalConvention
|
|
1145
1351
|
) {
|
|
1146
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();
|
|
1147
1370
|
};
|
|
1148
1371
|
|
|
1149
1372
|
Niivue.prototype.getRadiologicalConvention = function () {
|
|
@@ -1161,7 +1384,7 @@ Niivue.prototype.addVolume = function (volume) {
|
|
|
1161
1384
|
this.volumes.push(volume);
|
|
1162
1385
|
let idx = this.volumes.length === 1 ? 0 : this.volumes.length - 1;
|
|
1163
1386
|
this.setVolume(volume, idx);
|
|
1164
|
-
this.
|
|
1387
|
+
this.opts.onImageLoaded(volume);
|
|
1165
1388
|
};
|
|
1166
1389
|
|
|
1167
1390
|
/**
|
|
@@ -1175,7 +1398,7 @@ Niivue.prototype.addMesh = function (mesh) {
|
|
|
1175
1398
|
this.meshes.push(mesh);
|
|
1176
1399
|
let idx = this.meshes.length === 1 ? 0 : this.meshes.length - 1;
|
|
1177
1400
|
this.setMesh(mesh, idx);
|
|
1178
|
-
this.
|
|
1401
|
+
this.opts.onImageLoaded(mesh);
|
|
1179
1402
|
};
|
|
1180
1403
|
|
|
1181
1404
|
/**
|
|
@@ -1196,8 +1419,165 @@ Niivue.prototype.getVolumeIndexByID = function (id) {
|
|
|
1196
1419
|
return -1; // -1 signals that no valid index was found for a volume with the given id
|
|
1197
1420
|
};
|
|
1198
1421
|
|
|
1422
|
+
//inputs
|
|
1423
|
+
// data: Uint8Array to compress
|
|
1424
|
+
//output
|
|
1425
|
+
// returns rle compressed Uint8Array
|
|
1426
|
+
|
|
1427
|
+
function encodeRLE(data) {
|
|
1428
|
+
//https://en.wikipedia.org/wiki/PackBits
|
|
1429
|
+
//run length encoding
|
|
1430
|
+
// input and output are Uint8Array
|
|
1431
|
+
// Will compress data with long runs up to x64
|
|
1432
|
+
// Worst case encoded size is ~1% larger than input
|
|
1433
|
+
let dl = data.length; //input length
|
|
1434
|
+
let dp = 0; //input position
|
|
1435
|
+
//worst case: run length encoding (1+1/127) times larger than input
|
|
1436
|
+
let r = new Uint8Array(dl + Math.ceil(0.01 * dl));
|
|
1437
|
+
let rI = new Int8Array(r.buffer); //typecast as header can be negative
|
|
1438
|
+
let rp = 0; //run length position
|
|
1439
|
+
while (dp < dl) {
|
|
1440
|
+
//for each byte in input
|
|
1441
|
+
let v = data[dp];
|
|
1442
|
+
dp++;
|
|
1443
|
+
let rl = 1; //run length
|
|
1444
|
+
while (rl < 129 && dp < dl && data[dp] === v) {
|
|
1445
|
+
dp++;
|
|
1446
|
+
rl++;
|
|
1447
|
+
} //count run length
|
|
1448
|
+
if (rl > 1) {
|
|
1449
|
+
//header
|
|
1450
|
+
rI[rp] = -rl + 1;
|
|
1451
|
+
rp++;
|
|
1452
|
+
r[rp] = v;
|
|
1453
|
+
rp++;
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
//count literal length
|
|
1457
|
+
while (dp < dl) {
|
|
1458
|
+
if (rl > 127) break;
|
|
1459
|
+
if (dp + 2 < dl) {
|
|
1460
|
+
//console.log(':', v, data[dp], data[dp+1]);
|
|
1461
|
+
if (
|
|
1462
|
+
v !== data[dp] &&
|
|
1463
|
+
data[dp + 2] === data[dp] &&
|
|
1464
|
+
data[dp + 1] === data[dp]
|
|
1465
|
+
)
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
v = data[dp];
|
|
1469
|
+
dp++;
|
|
1470
|
+
rl++;
|
|
1471
|
+
}
|
|
1472
|
+
//write header
|
|
1473
|
+
r[rp] = rl - 1;
|
|
1474
|
+
rp++;
|
|
1475
|
+
for (let i = 0; i < rl; i++) {
|
|
1476
|
+
r[rp] = data[dp - rl + i];
|
|
1477
|
+
rp++;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
log.info("PackBits " + dl + " -> " + rp + " bytes (x" + dl / rp + ")");
|
|
1481
|
+
return r.slice(0, rp);
|
|
1482
|
+
} // encodeRLE()
|
|
1483
|
+
|
|
1484
|
+
//inputs
|
|
1485
|
+
// rle: packbits compressed stream
|
|
1486
|
+
// decodedlen: size of uncompressed data
|
|
1487
|
+
//output
|
|
1488
|
+
// returns Uint8Array of decodedlen bytes
|
|
1489
|
+
function decodeRLE(rle, decodedlen) {
|
|
1490
|
+
let r = new Uint8Array(rle.buffer);
|
|
1491
|
+
let rI = new Int8Array(r.buffer); //typecast as header can be negative
|
|
1492
|
+
let rp = 0; //input position in rle array
|
|
1493
|
+
//d: output uncompressed data array
|
|
1494
|
+
let d = new Uint8Array(decodedlen);
|
|
1495
|
+
let dp = 0; //output position in decoded array
|
|
1496
|
+
while (rp < r.length) {
|
|
1497
|
+
//read header
|
|
1498
|
+
let hdr = rI[rp];
|
|
1499
|
+
rp++;
|
|
1500
|
+
if (hdr < 0) {
|
|
1501
|
+
//write run
|
|
1502
|
+
let v = rI[rp];
|
|
1503
|
+
rp++;
|
|
1504
|
+
for (let i = 0; i < 1 - hdr; i++) {
|
|
1505
|
+
d[dp] = v;
|
|
1506
|
+
dp++;
|
|
1507
|
+
}
|
|
1508
|
+
} else {
|
|
1509
|
+
//write literal
|
|
1510
|
+
for (let i = 0; i < hdr + 1; i++) {
|
|
1511
|
+
d[dp] = rI[rp];
|
|
1512
|
+
rp++;
|
|
1513
|
+
dp++;
|
|
1514
|
+
}
|
|
1515
|
+
} //if run else literal
|
|
1516
|
+
} //while rp < r.length
|
|
1517
|
+
return d;
|
|
1518
|
+
} // decodeRLE()
|
|
1519
|
+
|
|
1520
|
+
/*Niivue.prototype.testRLE = function() {
|
|
1521
|
+
//Demo to test encodeRLE/decodeRLE
|
|
1522
|
+
let len = 256*256*256;
|
|
1523
|
+
let data = new Uint8Array(len);
|
|
1524
|
+
for (let i = 0; i < len; i++)
|
|
1525
|
+
data[i] = ((Math.random() > 0.30) ? 1 : 0);
|
|
1526
|
+
console.log(data);
|
|
1527
|
+
let rle = encodeRLE(data);
|
|
1528
|
+
let ddata = decodeRLE(rle, len);
|
|
1529
|
+
let ok = true;
|
|
1530
|
+
for (let i = 0; i < len; i++)
|
|
1531
|
+
if (data[i] !== ddata[i])
|
|
1532
|
+
ok = false;
|
|
1533
|
+
console.log('decoded correctly:', ok);
|
|
1534
|
+
} // testRLE()*/
|
|
1535
|
+
|
|
1536
|
+
Niivue.prototype.drawAddUndoBitmap = async function (fnm) {
|
|
1537
|
+
if (!this.drawBitmap || this.drawBitmap.length < 1) {
|
|
1538
|
+
console.log("drawAddUndoBitmap error: No drawing open");
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
//let rle = encodeRLE(this.drawBitmap);
|
|
1542
|
+
//the bitmaps are a cyclical loop, like a revolver hand gun: increment the cylinder
|
|
1543
|
+
this.currentDrawUndoBitmap++;
|
|
1544
|
+
if (this.currentDrawUndoBitmap >= this.maxDrawUndoBitmaps)
|
|
1545
|
+
this.currentDrawUndoBitmap = 0;
|
|
1546
|
+
this.drawUndoBitmaps[this.currentDrawUndoBitmap] = encodeRLE(this.drawBitmap);
|
|
1547
|
+
}; // drawAddUndoBitmap()
|
|
1548
|
+
|
|
1549
|
+
Niivue.prototype.drawClearAllUndoBitmaps = async function () {
|
|
1550
|
+
this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //next add will be cylinder 0
|
|
1551
|
+
if (this.drawUndoBitmaps.length < 1) return;
|
|
1552
|
+
for (let i = this.drawUndoBitmaps.length - 1; i >= 0; i--) array[i] = [];
|
|
1553
|
+
}; // drawClearAllUndoBitmaps()
|
|
1554
|
+
|
|
1555
|
+
Niivue.prototype.drawUndo = function () {
|
|
1556
|
+
if (this.drawUndoBitmaps.length < 1) {
|
|
1557
|
+
console.log("undo bitmaps not loaded");
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
//let dims = this.back.hdr.dims;
|
|
1561
|
+
//let nv = dims[1] * dims[2] * dims[3];
|
|
1562
|
+
this.currentDrawUndoBitmap--;
|
|
1563
|
+
if (this.currentDrawUndoBitmap < 0)
|
|
1564
|
+
this.currentDrawUndoBitmap = this.drawUndoBitmaps.length - 1;
|
|
1565
|
+
if (this.currentDrawUndoBitmap >= this.drawUndoBitmaps.length)
|
|
1566
|
+
this.currentDrawUndoBitmap = 0;
|
|
1567
|
+
if (this.drawUndoBitmaps[this.currentDrawUndoBitmap].length < 2) {
|
|
1568
|
+
console.log("drawUndo is misbehaving");
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
this.drawBitmap = decodeRLE(
|
|
1572
|
+
this.drawUndoBitmaps[this.currentDrawUndoBitmap],
|
|
1573
|
+
this.drawBitmap.length
|
|
1574
|
+
);
|
|
1575
|
+
this.refreshDrawing(true);
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1199
1578
|
Niivue.prototype.loadDrawing = async function (fnm) {
|
|
1200
1579
|
if (this.drawBitmap) console.log("Overwriting open drawing!");
|
|
1580
|
+
this.drawClearAllUndoBitmaps();
|
|
1201
1581
|
let volume = await NVImage.loadFromUrl({ url: fnm });
|
|
1202
1582
|
//let volume = await NVImage.loadFromFile({file: fnm});
|
|
1203
1583
|
let dims = volume.hdr.dims; //reverse to original
|
|
@@ -1215,7 +1595,7 @@ Niivue.prototype.loadDrawing = async function (fnm) {
|
|
|
1215
1595
|
//if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {
|
|
1216
1596
|
let vx = dims[1] * dims[2] * dims[3];
|
|
1217
1597
|
//this.drawBitmap = new Uint8Array(vx);
|
|
1218
|
-
this.drawUndoBitmap = null;
|
|
1598
|
+
//this.drawUndoBitmap = null;
|
|
1219
1599
|
this.drawBitmap = new Uint8Array(vx);
|
|
1220
1600
|
this.drawTexture = this.r8Tex(
|
|
1221
1601
|
this.drawTexture,
|
|
@@ -1273,6 +1653,7 @@ Niivue.prototype.loadDrawing = async function (fnm) {
|
|
|
1273
1653
|
} //for x
|
|
1274
1654
|
} //for y
|
|
1275
1655
|
} //for z
|
|
1656
|
+
this.drawAddUndoBitmap();
|
|
1276
1657
|
this.refreshDrawing(false);
|
|
1277
1658
|
this.drawScene();
|
|
1278
1659
|
};
|
|
@@ -1381,6 +1762,7 @@ Niivue.prototype.reverseFaces = function (mesh) {
|
|
|
1381
1762
|
this.meshes[idx].reverseFaces(this.gl);
|
|
1382
1763
|
this.updateGLVolume();
|
|
1383
1764
|
};
|
|
1765
|
+
|
|
1384
1766
|
Niivue.prototype.setMeshLayerProperty = function (mesh, layer, key, val) {
|
|
1385
1767
|
let idx = this.getMeshIndexByID(mesh);
|
|
1386
1768
|
if (idx < 0) {
|
|
@@ -1391,17 +1773,21 @@ Niivue.prototype.setMeshLayerProperty = function (mesh, layer, key, val) {
|
|
|
1391
1773
|
this.updateGLVolume();
|
|
1392
1774
|
};
|
|
1393
1775
|
|
|
1776
|
+
Niivue.prototype.setPan2Dxyzmm = function (xyzmmZoom) {
|
|
1777
|
+
this.scene.pan2Dxyzmm = xyzmmZoom;
|
|
1778
|
+
this.drawScene();
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1394
1781
|
Niivue.prototype.setScale2D = function (zoomFactor) {
|
|
1395
1782
|
this.scene.scale2D = zoomFactor;
|
|
1396
|
-
console.log("bingo");
|
|
1397
1783
|
this.drawScene();
|
|
1398
|
-
}; //
|
|
1784
|
+
}; // setScale2D()
|
|
1399
1785
|
|
|
1400
1786
|
Niivue.prototype.setRenderAzimuthElevation = function (a, e) {
|
|
1401
1787
|
this.scene.renderAzimuth = a;
|
|
1402
1788
|
this.scene.renderElevation = e;
|
|
1403
1789
|
this.drawScene();
|
|
1404
|
-
}; //
|
|
1790
|
+
}; // setRenderAzimuthElevation()
|
|
1405
1791
|
|
|
1406
1792
|
/**
|
|
1407
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.
|
|
@@ -1548,14 +1934,14 @@ Niivue.prototype.moveVolumeToTop = function (volume) {
|
|
|
1548
1934
|
// update mouse position from new mouse down coordinates
|
|
1549
1935
|
// note: no test yet
|
|
1550
1936
|
Niivue.prototype.mouseDown = function mouseDown(x, y) {
|
|
1551
|
-
if (this.
|
|
1937
|
+
if (this.inRenderTile(x, y) < 0) return;
|
|
1552
1938
|
this.mousePos = [x, y];
|
|
1553
1939
|
}; // mouseDown()
|
|
1554
1940
|
|
|
1555
1941
|
// not included in public docs
|
|
1556
1942
|
// note: no test yet
|
|
1557
1943
|
Niivue.prototype.mouseMove = function mouseMove(x, y) {
|
|
1558
|
-
if (this.
|
|
1944
|
+
if (this.inRenderTile(x, y) < 0) return;
|
|
1559
1945
|
this.scene.renderAzimuth += x - this.mousePos[0];
|
|
1560
1946
|
this.scene.renderElevation += y - this.mousePos[1];
|
|
1561
1947
|
this.mousePos = [x, y];
|
|
@@ -1608,7 +1994,7 @@ Niivue.prototype.setClipPlane = function (depthAzimuthElevation) {
|
|
|
1608
1994
|
);
|
|
1609
1995
|
this.scene.clipPlane = [v[0], v[1], v[2], depthAzimuthElevation[0]];
|
|
1610
1996
|
this.scene.clipPlaneDepthAziElev = depthAzimuthElevation;
|
|
1611
|
-
if (this.sliceType != this.sliceTypeRender) return;
|
|
1997
|
+
//if (this.sliceType != this.sliceTypeRender) return;
|
|
1612
1998
|
this.drawScene();
|
|
1613
1999
|
}; // setClipPlane()
|
|
1614
2000
|
|
|
@@ -1715,6 +2101,11 @@ Niivue.prototype.setScale = function (scale) {
|
|
|
1715
2101
|
*/
|
|
1716
2102
|
Niivue.prototype.setClipPlaneColor = function (color) {
|
|
1717
2103
|
this.opts.clipPlaneColor = color;
|
|
2104
|
+
this.renderShader.use(this.gl);
|
|
2105
|
+
this.gl.uniform4fv(
|
|
2106
|
+
this.renderShader.uniforms["clipPlaneColor"],
|
|
2107
|
+
this.opts.clipPlaneColor
|
|
2108
|
+
);
|
|
1718
2109
|
this.drawScene();
|
|
1719
2110
|
}; // setClipPlaneColor()
|
|
1720
2111
|
|
|
@@ -1805,6 +2196,7 @@ Niivue.prototype.loadVolumes = async function (volumeList) {
|
|
|
1805
2196
|
url: volumeList[i].url,
|
|
1806
2197
|
name: volumeList[i].name,
|
|
1807
2198
|
colorMap: volumeList[i].colorMap,
|
|
2199
|
+
colorMapNegative: volumeList[i].colorMapNegative,
|
|
1808
2200
|
opacity: volumeList[i].opacity,
|
|
1809
2201
|
urlImgData: volumeList[i].urlImgData,
|
|
1810
2202
|
cal_min: volumeList[i].cal_min,
|
|
@@ -1915,7 +2307,9 @@ Niivue.prototype.createEmptyDrawing = function () {
|
|
|
1915
2307
|
if (mn < 1) return; //something is horribly wrong!
|
|
1916
2308
|
let vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
|
|
1917
2309
|
this.drawBitmap = new Uint8Array(vx);
|
|
1918
|
-
this.
|
|
2310
|
+
this.drawClearAllUndoBitmaps();
|
|
2311
|
+
this.drawAddUndoBitmap();
|
|
2312
|
+
//this.drawUndoBitmap = null;
|
|
1919
2313
|
this.drawTexture = this.r8Tex(
|
|
1920
2314
|
this.drawTexture,
|
|
1921
2315
|
this.gl.TEXTURE7,
|
|
@@ -2038,16 +2432,6 @@ function img2ras16(volume) {
|
|
|
2038
2432
|
return img16;
|
|
2039
2433
|
}
|
|
2040
2434
|
|
|
2041
|
-
Niivue.prototype.drawUndo = function () {
|
|
2042
|
-
let hdr = this.back.hdr;
|
|
2043
|
-
let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
2044
|
-
if (this.drawUndoBitmap.length !== nv || this.drawBitmap.length !== nv) {
|
|
2045
|
-
console.log("bitmap dims are wrong");
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
this.drawBitmap = this.drawUndoBitmap.slice();
|
|
2049
|
-
this.refreshDrawing(true);
|
|
2050
|
-
};
|
|
2051
2435
|
Niivue.prototype.drawGrowCut = function () {
|
|
2052
2436
|
let hdr = this.back.hdr;
|
|
2053
2437
|
let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
@@ -2055,7 +2439,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2055
2439
|
console.log("bitmap dims are wrong");
|
|
2056
2440
|
return;
|
|
2057
2441
|
}
|
|
2058
|
-
this.drawUndoBitmap = this.drawBitmap.slice();
|
|
2442
|
+
//this.drawUndoBitmap = this.drawBitmap.slice();
|
|
2059
2443
|
let gl = this.gl;
|
|
2060
2444
|
let fb = gl.createFramebuffer();
|
|
2061
2445
|
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
|
@@ -2068,15 +2452,17 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2068
2452
|
for (let i = 1; i < nv; i++) img16[i] = this.drawBitmap[i];
|
|
2069
2453
|
let label0 = this.r16Tex(null, gl.TEXTURE6, this.back.dims, img16);
|
|
2070
2454
|
let label1 = this.r16Tex(null, gl.TEXTURE7, this.back.dims, img16); //TEXTURE7 = draw Texture
|
|
2071
|
-
|
|
2455
|
+
let kMAX_STRENGTH = 10000;
|
|
2456
|
+
for (let i = 1; i < nv; i++) if (img16[i] > 0) img16[i] = kMAX_STRENGTH;
|
|
2072
2457
|
let strength0 = this.r16Tex(null, gl.TEXTURE4, this.back.dims, img16);
|
|
2073
2458
|
let strength1 = this.r16Tex(null, gl.TEXTURE5, this.back.dims, img16);
|
|
2074
2459
|
this.gl.bindVertexArray(this.genericVAO);
|
|
2075
2460
|
let shader = this.growCutShader;
|
|
2076
2461
|
shader.use(gl);
|
|
2462
|
+
let iterations = 128; //will run 2x this value
|
|
2463
|
+
gl.uniform1i(shader.uniforms["finalPass"], 0);
|
|
2077
2464
|
gl.uniform1i(shader.uniforms["inputTexture0"], 3); // background is TEXTURE3
|
|
2078
|
-
|
|
2079
|
-
for (let j = 0; j < 45; j++) {
|
|
2465
|
+
for (let j = 0; j < iterations; j++) {
|
|
2080
2466
|
gl.uniform1i(shader.uniforms["inputTexture1"], 6); // label0 is TEXTURE6
|
|
2081
2467
|
gl.uniform1i(shader.uniforms["inputTexture2"], 4); // strength0 is TEXTURE4
|
|
2082
2468
|
for (let i = 0; i < this.back.dims[3]; i++) {
|
|
@@ -2104,6 +2490,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2104
2490
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
2105
2491
|
} //for i: each slice
|
|
2106
2492
|
//reverse order strength1/label1 and strength0/label0 for reading and writing:
|
|
2493
|
+
if (j === iterations - 1) gl.uniform1i(shader.uniforms["finalPass"], 1);
|
|
2107
2494
|
gl.uniform1i(shader.uniforms["inputTexture1"], 7); // label1 is TEXTURE7
|
|
2108
2495
|
gl.uniform1i(shader.uniforms["inputTexture2"], 5); // strength1 is TEXTURE5
|
|
2109
2496
|
for (let i = 0; i < this.back.dims[3]; i++) {
|
|
@@ -2132,17 +2519,9 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2132
2519
|
} //for j: each iteration
|
|
2133
2520
|
//read data
|
|
2134
2521
|
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
|
|
2135
|
-
let readAttach = gl.
|
|
2136
|
-
let readTex =
|
|
2137
|
-
let readStrength = false;
|
|
2138
|
-
if (readStrength) {
|
|
2139
|
-
//read strength or label
|
|
2140
|
-
//readAttach = gl.COLOR_ATTACHMENT1;
|
|
2141
|
-
readTex = strength1;
|
|
2142
|
-
}
|
|
2143
|
-
//gl.readBuffer(gl.COLOR_ATTACHMENT1); //strength
|
|
2522
|
+
let readAttach = gl.COLOR_ATTACHMENT1;
|
|
2523
|
+
let readTex = label0;
|
|
2144
2524
|
gl.readBuffer(readAttach); //label
|
|
2145
|
-
|
|
2146
2525
|
// assuming a framebuffer is bound with the texture to read attached
|
|
2147
2526
|
const format = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT);
|
|
2148
2527
|
const type = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE);
|
|
@@ -2173,12 +2552,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2173
2552
|
}
|
|
2174
2553
|
let mx = img16[0];
|
|
2175
2554
|
for (let i = 0; i < img16.length; i++) mx = Math.max(mx, img16[i]);
|
|
2176
|
-
//let img16 = new Int16Array(nv);
|
|
2177
|
-
//gl.readPixels(0, 0, w, h, format, type);
|
|
2178
|
-
// this.fallbackReadPixelsFormat, this.fallbackReadPixelsType, fallbackSliceView);
|
|
2179
|
-
|
|
2180
2555
|
for (let i = 1; i < nv; i++) this.drawBitmap[i] = img16[i];
|
|
2181
|
-
|
|
2182
2556
|
//clean up
|
|
2183
2557
|
//restore textures
|
|
2184
2558
|
gl.activeTexture(gl.TEXTURE2);
|
|
@@ -2199,6 +2573,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2199
2573
|
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
2200
2574
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2201
2575
|
gl.deleteFramebuffer(fb);
|
|
2576
|
+
this.drawAddUndoBitmap();
|
|
2202
2577
|
this.refreshDrawing(true);
|
|
2203
2578
|
};
|
|
2204
2579
|
|
|
@@ -2215,7 +2590,7 @@ Niivue.prototype.drawPt = function (x, y, z, penValue) {
|
|
|
2215
2590
|
//https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
|
|
2216
2591
|
// https://www.geeksforgeeks.org/bresenhams-algorithm-for-3-d-line-drawing/
|
|
2217
2592
|
// ptA, ptB are start and end points of line (each XYZ)
|
|
2218
|
-
Niivue.prototype.
|
|
2593
|
+
Niivue.prototype.drawPenLine = function (ptA, ptB, penValue) {
|
|
2219
2594
|
let dx = Math.abs(ptA[0] - ptB[0]);
|
|
2220
2595
|
let dy = Math.abs(ptA[1] - ptB[1]);
|
|
2221
2596
|
let dz = Math.abs(ptA[2] - ptB[2]);
|
|
@@ -2289,6 +2664,87 @@ Niivue.prototype.drawLine = function (ptA, ptB, penValue) {
|
|
|
2289
2664
|
}
|
|
2290
2665
|
};
|
|
2291
2666
|
|
|
2667
|
+
Niivue.prototype.drawFloodFill = function (seedXYZ, newColor = 0) {
|
|
2668
|
+
//3D "paint bucket" fill:
|
|
2669
|
+
// set all voxels connected to seed point to newColor
|
|
2670
|
+
// https://en.wikipedia.org/wiki/Flood_fill
|
|
2671
|
+
newColor = Math.abs(newColor);
|
|
2672
|
+
let dims = [this.back.dims[1], this.back.dims[2], this.back.dims[3]]; //+1: dims indexed from 0!
|
|
2673
|
+
if (seedXYZ[0] < 0 || seedXYZ[1] < 0 || seedXYZ[2] < 0) return;
|
|
2674
|
+
if (seedXYZ[0] >= dims[0] || seedXYZ[1] >= dims[1] || seedXYZ[2] >= dims[2])
|
|
2675
|
+
return;
|
|
2676
|
+
let nx = dims[0];
|
|
2677
|
+
let nxy = nx * dims[1];
|
|
2678
|
+
let nxyz = nxy * dims[2];
|
|
2679
|
+
let img = this.drawBitmap.slice();
|
|
2680
|
+
if (img.length !== nxy * dims[2]) return;
|
|
2681
|
+
function xyz2vx(pt) {
|
|
2682
|
+
//provided an XYZ 3D point, provide address in 1D array
|
|
2683
|
+
return pt[0] + pt[1] * nx + pt[2] * nxy;
|
|
2684
|
+
}
|
|
2685
|
+
function vx2xyz(vx) {
|
|
2686
|
+
//provided address in 1D array, return XYZ coordinate
|
|
2687
|
+
let Z = Math.floor(vx / nxy); //slice
|
|
2688
|
+
let Y = Math.floor((vx - Z * nxy) / nx); //column
|
|
2689
|
+
let X = Math.floor(vx % nx);
|
|
2690
|
+
return [X, Y, Z];
|
|
2691
|
+
}
|
|
2692
|
+
let seedVx = xyz2vx(seedXYZ);
|
|
2693
|
+
let seedColor = img[seedVx];
|
|
2694
|
+
if (seedColor === newColor) {
|
|
2695
|
+
console.log("drawFloodFill selected voxel is already desired color");
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
//img is binary mask with selected color as 1 and all other colors 0
|
|
2699
|
+
for (let i = 1; i < nxyz; i++) {
|
|
2700
|
+
img[i] = 0;
|
|
2701
|
+
if (this.drawBitmap[i] === seedColor) img[i] = 1;
|
|
2702
|
+
}
|
|
2703
|
+
//1. Set Q to the empty queue or stack.
|
|
2704
|
+
let Q = [];
|
|
2705
|
+
//2. Add node to the end of Q.
|
|
2706
|
+
Q.push(seedVx);
|
|
2707
|
+
img[seedVx] = 2; //part of cluster
|
|
2708
|
+
//3. While Q is not empty:
|
|
2709
|
+
while (Q.length > 0) {
|
|
2710
|
+
//4. Set n equal to the first element of Q.
|
|
2711
|
+
let vx = Q[0];
|
|
2712
|
+
//5. Remove first element from Q.
|
|
2713
|
+
Q.shift();
|
|
2714
|
+
//6. Test six neighbors of n (left,right,anterior,posterior,inferior, superior
|
|
2715
|
+
// If any is is unfound part of cluster (value = 1) set it to found (value 2) and add to Q
|
|
2716
|
+
let xyz = vx2xyz(vx);
|
|
2717
|
+
function testNeighbor(offset) {
|
|
2718
|
+
let xyzN = xyz.slice();
|
|
2719
|
+
xyzN[0] += offset[0];
|
|
2720
|
+
xyzN[1] += offset[1];
|
|
2721
|
+
xyzN[2] += offset[2];
|
|
2722
|
+
if (xyzN[0] < 0 || xyzN[1] < 0 || xyzN[2] < 0) return;
|
|
2723
|
+
if (xyzN[0] >= dims[0] || xyzN[1] >= dims[1] || xyzN[2] >= dims[2])
|
|
2724
|
+
return;
|
|
2725
|
+
let vxT = xyz2vx(xyzN);
|
|
2726
|
+
if (img[vxT] !== 1) return;
|
|
2727
|
+
img[vxT] = 2; //part of cluster
|
|
2728
|
+
Q.push(vxT);
|
|
2729
|
+
}
|
|
2730
|
+
testNeighbor([0, 0, -1]); //inferior
|
|
2731
|
+
testNeighbor([0, 0, 1]); //superior
|
|
2732
|
+
testNeighbor([0, -1, 0]); //posterior
|
|
2733
|
+
testNeighbor([0, 1, 0]); //anterior
|
|
2734
|
+
testNeighbor([-1, 0, 0]); //left
|
|
2735
|
+
testNeighbor([1, 0, 0]); //right
|
|
2736
|
+
//7. Continue looping until Q is exhausted.
|
|
2737
|
+
}
|
|
2738
|
+
//8. Return
|
|
2739
|
+
//this.drawUndoBitmap = this.drawBitmap.slice();
|
|
2740
|
+
for (let i = 1; i < nxyz; i++)
|
|
2741
|
+
if (img[i] === 2)
|
|
2742
|
+
//if part of cluster
|
|
2743
|
+
this.drawBitmap[i] = newColor;
|
|
2744
|
+
this.drawAddUndoBitmap();
|
|
2745
|
+
this.refreshDrawing(false);
|
|
2746
|
+
}; // drawFloodFill()
|
|
2747
|
+
|
|
2292
2748
|
Niivue.prototype.drawPenFilled = function () {
|
|
2293
2749
|
let nPts = this.drawPenFillPts.length;
|
|
2294
2750
|
if (nPts < 2) {
|
|
@@ -2415,17 +2871,20 @@ Niivue.prototype.drawPenFilled = function () {
|
|
|
2415
2871
|
} // x column
|
|
2416
2872
|
} //y row
|
|
2417
2873
|
} //not axial
|
|
2874
|
+
//this.drawUndoBitmaps[this.currentDrawUndoBitmap]
|
|
2418
2875
|
if (
|
|
2419
2876
|
!this.drawFillOverwrites &&
|
|
2420
|
-
this.
|
|
2877
|
+
this.drawUndoBitmaps[this.currentDrawUndoBitmap].length > 0
|
|
2421
2878
|
) {
|
|
2422
|
-
let nv = this.
|
|
2879
|
+
let nv = this.drawBitmap.length;
|
|
2880
|
+
let bmp = decodeRLE(this.drawUndoBitmaps[this.currentDrawUndoBitmap], nv);
|
|
2423
2881
|
for (let i = 0; i < nv; i++) {
|
|
2424
|
-
if (
|
|
2425
|
-
this.drawBitmap[i] =
|
|
2882
|
+
if (bmp[i] === 0) continue;
|
|
2883
|
+
this.drawBitmap[i] = bmp[i];
|
|
2426
2884
|
}
|
|
2427
2885
|
}
|
|
2428
2886
|
this.drawPenFillPts = [];
|
|
2887
|
+
this.drawAddUndoBitmap();
|
|
2429
2888
|
this.refreshDrawing(false);
|
|
2430
2889
|
}; // drawPenFilled()
|
|
2431
2890
|
|
|
@@ -2880,13 +3339,28 @@ Niivue.prototype.init = async function () {
|
|
|
2880
3339
|
this.sliceShader = new Shader(this.gl, vertSliceShader, fragSliceShader);
|
|
2881
3340
|
this.sliceShader.use(this.gl);
|
|
2882
3341
|
this.gl.uniform1i(this.sliceShader.uniforms["volume"], 0);
|
|
2883
|
-
//this.gl.uniform1i(this.sliceShader.uniforms["colormap"], 1); //orient shader applies colormap
|
|
2884
3342
|
this.gl.uniform1i(this.sliceShader.uniforms["overlay"], 2);
|
|
2885
3343
|
this.gl.uniform1i(this.sliceShader.uniforms["drawing"], 7);
|
|
2886
3344
|
this.gl.uniform1f(this.sliceShader.uniforms["drawOpacity"], this.drawOpacity);
|
|
2887
|
-
//
|
|
2888
|
-
this.
|
|
2889
|
-
|
|
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);
|
|
2890
3364
|
// render shader (3D)
|
|
2891
3365
|
this.renderShader = new Shader(this.gl, vertRenderShader, fragRenderShader);
|
|
2892
3366
|
this.renderShader.use(this.gl);
|
|
@@ -3115,7 +3589,7 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
|
|
|
3115
3589
|
if (layer === 0) {
|
|
3116
3590
|
this.volumeObject3D = overlayItem.toNiivueObject3D(this.VOLUME_ID, this.gl);
|
|
3117
3591
|
mat.mat4.invert(mtx, mtx);
|
|
3118
|
-
log.debug(`mtx layer ${layer}`, mtx);
|
|
3592
|
+
//log.debug(`mtx layer ${layer}`, mtx);
|
|
3119
3593
|
this.back.matRAS = overlayItem.matRAS;
|
|
3120
3594
|
this.back.dims = overlayItem.dimsRAS;
|
|
3121
3595
|
this.back.pixDims = overlayItem.pixDimsRAS;
|
|
@@ -3428,10 +3902,81 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
|
|
|
3428
3902
|
this.gl.uniform1i(orientShader.uniforms["blend3D"], 5);
|
|
3429
3903
|
this.gl.uniform1i(orientShader.uniforms["colormap"], 1);
|
|
3430
3904
|
this.gl.uniform1f(orientShader.uniforms["layer"], layer);
|
|
3431
|
-
this.gl.uniform1f(orientShader.uniforms["numLayers"], numLayers);
|
|
3905
|
+
//this.gl.uniform1f(orientShader.uniforms["numLayers"], numLayers);
|
|
3432
3906
|
this.gl.uniform1f(orientShader.uniforms["scl_inter"], hdr.scl_inter);
|
|
3433
3907
|
this.gl.uniform1f(orientShader.uniforms["scl_slope"], hdr.scl_slope);
|
|
3434
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);
|
|
3435
3980
|
this.gl.uniformMatrix4fv(orientShader.uniforms["mtx"], false, mtx);
|
|
3436
3981
|
if (hdr.intent_code === 1002) {
|
|
3437
3982
|
let x = 1.0 / this.back.dims[1];
|
|
@@ -3459,6 +4004,7 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
|
|
|
3459
4004
|
}
|
|
3460
4005
|
this.gl.bindVertexArray(this.unusedVAO);
|
|
3461
4006
|
this.gl.deleteTexture(tempTex3D);
|
|
4007
|
+
this.gl.deleteTexture(modulateTexture);
|
|
3462
4008
|
this.gl.deleteTexture(blendTexture);
|
|
3463
4009
|
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
|
|
3464
4010
|
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
@@ -3491,6 +4037,16 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
|
|
|
3491
4037
|
this.overlays.length
|
|
3492
4038
|
);
|
|
3493
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);
|
|
3494
4050
|
// set overlays for slice shader
|
|
3495
4051
|
this.sliceShader.use(this.gl);
|
|
3496
4052
|
this.gl.uniform1f(
|
|
@@ -3499,6 +4055,8 @@ Niivue.prototype.refreshLayers = function (overlayItem, layer, numLayers) {
|
|
|
3499
4055
|
);
|
|
3500
4056
|
this.gl.uniform1f(this.sliceShader.uniforms["drawOpacity"], this.drawOpacity);
|
|
3501
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);
|
|
3502
4060
|
this.updateInterpolation(layer);
|
|
3503
4061
|
//this.createEmptyDrawing(); //DO NOT DO THIS ON EVERY CALL TO REFRESH LAYERS!!!!
|
|
3504
4062
|
//this.createRandomDrawing(); //DO NOT DO THIS ON EVERY CALL TO REFRESH LAYERS!!!!
|
|
@@ -3530,6 +4088,24 @@ Niivue.prototype.setColorMap = function (id, colorMap) {
|
|
|
3530
4088
|
this.updateGLVolume();
|
|
3531
4089
|
};
|
|
3532
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
|
+
};
|
|
3533
4109
|
Niivue.prototype.setGamma = function (gamma = 1.0) {
|
|
3534
4110
|
cmapper.gamma = gamma;
|
|
3535
4111
|
this.updateGLVolume();
|
|
@@ -3573,7 +4149,7 @@ Niivue.prototype.refreshColormaps = function () {
|
|
|
3573
4149
|
this.colormapTexture = this.gl.createTexture();
|
|
3574
4150
|
this.gl.activeTexture(this.gl.TEXTURE1);
|
|
3575
4151
|
this.gl.bindTexture(this.gl.TEXTURE_2D, this.colormapTexture);
|
|
3576
|
-
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);
|
|
3577
4153
|
this.gl.texParameteri(
|
|
3578
4154
|
this.gl.TEXTURE_2D,
|
|
3579
4155
|
this.gl.TEXTURE_MIN_FILTER,
|
|
@@ -3598,22 +4174,38 @@ Niivue.prototype.refreshColormaps = function () {
|
|
|
3598
4174
|
);
|
|
3599
4175
|
this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT, 1);
|
|
3600
4176
|
let luts = this.colormap(this.volumes[0].colorMap);
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
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
|
|
3610
4202
|
this.gl.texSubImage2D(
|
|
3611
4203
|
this.gl.TEXTURE_2D,
|
|
3612
4204
|
0,
|
|
3613
4205
|
0,
|
|
3614
4206
|
0,
|
|
3615
4207
|
256,
|
|
3616
|
-
nLayer,
|
|
4208
|
+
nLayer * 2,
|
|
3617
4209
|
this.gl.RGBA,
|
|
3618
4210
|
this.gl.UNSIGNED_BYTE,
|
|
3619
4211
|
luts
|
|
@@ -3623,23 +4215,64 @@ Niivue.prototype.refreshColormaps = function () {
|
|
|
3623
4215
|
|
|
3624
4216
|
// not included in public docs
|
|
3625
4217
|
Niivue.prototype.sliceScale = function () {
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
this.back.dims[1] * this.back.pixDims[1],
|
|
3629
|
-
this.back.dims[2] * this.back.pixDims[2],
|
|
3630
|
-
this.back.dims[3] * this.back.pixDims[3],
|
|
3631
|
-
];
|
|
3632
|
-
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]));
|
|
3633
4220
|
var volScale = [
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
4221
|
+
dimsMM[0] / longestAxis,
|
|
4222
|
+
dimsMM[1] / longestAxis,
|
|
4223
|
+
dimsMM[2] / longestAxis,
|
|
3637
4224
|
];
|
|
3638
4225
|
var vox = [this.back.dims[1], this.back.dims[2], this.back.dims[3]];
|
|
3639
|
-
return { volScale, vox };
|
|
4226
|
+
return { volScale, vox, longestAxis, dimsMM };
|
|
3640
4227
|
}; // sliceScale()
|
|
3641
4228
|
|
|
3642
|
-
|
|
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
|
+
|
|
3643
4276
|
Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
3644
4277
|
var posNow;
|
|
3645
4278
|
var posFuture;
|
|
@@ -3674,34 +4307,14 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
|
3674
4307
|
return;
|
|
3675
4308
|
}
|
|
3676
4309
|
}
|
|
3677
|
-
if (this.sliceType === this.sliceTypeRender) {
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
if (this.volumes.length > 0 && this.scene.clipPlaneDepthAziElev[0] < 1.8) {
|
|
3682
|
-
//clipping mode: change clip plane depth
|
|
3683
|
-
//if (this.scene.clipPlaneDepthAziElev[0] > 1.8) return;
|
|
3684
|
-
let depthAziElev = this.scene.clipPlaneDepthAziElev.slice();
|
|
3685
|
-
//bound clip sqrt(3) = 1.73
|
|
3686
|
-
if (posChange > 0)
|
|
3687
|
-
depthAziElev[0] = Math.min(1.5, depthAziElev[0] + 0.025);
|
|
3688
|
-
if (posChange < 0)
|
|
3689
|
-
depthAziElev[0] = Math.max(-1.5, depthAziElev[0] - 0.025); //Math.max(-1.7,
|
|
3690
|
-
if (depthAziElev[0] !== this.scene.clipPlaneDepthAziElev[0]) {
|
|
3691
|
-
this.scene.clipPlaneDepthAziElev = depthAziElev;
|
|
3692
|
-
return this.setClipPlane(this.scene.clipPlaneDepthAziElev);
|
|
3693
|
-
}
|
|
3694
|
-
return;
|
|
3695
|
-
}
|
|
3696
|
-
if (posChange > 0)
|
|
3697
|
-
this.volScaleMultiplier = Math.min(2.0, this.volScaleMultiplier * 1.1);
|
|
3698
|
-
if (posChange < 0)
|
|
3699
|
-
this.volScaleMultiplier = Math.max(0.5, this.volScaleMultiplier * 0.9);
|
|
3700
|
-
this.drawScene();
|
|
4310
|
+
//if (this.sliceType === this.sliceTypeRender) {
|
|
4311
|
+
if (this.inRenderTile(x, y) >= 0) {
|
|
4312
|
+
this.sliceScroll3D(posChange);
|
|
4313
|
+
this.drawScene(); //TODO: twice?
|
|
3701
4314
|
return;
|
|
3702
4315
|
}
|
|
3703
4316
|
if (
|
|
3704
|
-
this.
|
|
4317
|
+
this.screenSlices.length < 1 ||
|
|
3705
4318
|
this.gl.canvas.height < 1 ||
|
|
3706
4319
|
this.gl.canvas.width < 1
|
|
3707
4320
|
)
|
|
@@ -3710,22 +4323,15 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
|
3710
4323
|
// webGL clip space L,R,T,B = [-1, 1, 1, 1]
|
|
3711
4324
|
// n.b. webGL Y polarity reversed
|
|
3712
4325
|
// https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html
|
|
3713
|
-
for (let i = 0; i < this.
|
|
4326
|
+
for (let i = 0; i < this.screenSlices.length; i++) {
|
|
3714
4327
|
var axCorSag = this.screenSlices[i].axCorSag;
|
|
3715
4328
|
if (this.drawPenAxCorSag >= 0 && this.drawPenAxCorSag !== axCorSag)
|
|
3716
4329
|
continue; //if mouse is drawing on axial slice, ignore any entry to coronal slice
|
|
3717
4330
|
if (axCorSag > this.sliceTypeSagittal) continue;
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
ltwh[0] += ltwh[2];
|
|
3723
|
-
ltwh[2] = -ltwh[2];
|
|
3724
|
-
}
|
|
3725
|
-
var fracX = (x - ltwh[0]) / ltwh[2];
|
|
3726
|
-
if (isMirror) fracX = 1.0 - fracX;
|
|
3727
|
-
var fracY = 1.0 - (y - ltwh[1]) / ltwh[3];
|
|
3728
|
-
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) {
|
|
3729
4335
|
//user clicked on slice i
|
|
3730
4336
|
if (!isDelta) {
|
|
3731
4337
|
this.scene.crosshairPos[2 - axCorSag] = posChange;
|
|
@@ -3740,38 +4346,31 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
|
3740
4346
|
if (posFuture < 0) posFuture = 0;
|
|
3741
4347
|
this.scene.crosshairPos[2 - axCorSag] = posFuture;
|
|
3742
4348
|
this.drawScene();
|
|
3743
|
-
this.
|
|
4349
|
+
this.opts.onLocationChange({
|
|
3744
4350
|
mm: this.frac2mm(this.scene.crosshairPos),
|
|
3745
4351
|
vox: this.frac2vox(this.scene.crosshairPos),
|
|
3746
4352
|
frac: this.scene.crosshairPos,
|
|
4353
|
+
xy: [x, y],
|
|
3747
4354
|
values: this.volumes.map((v) => {
|
|
3748
4355
|
let mm = this.frac2mm(this.scene.crosshairPos);
|
|
3749
4356
|
let vox = v.mm2vox(mm);
|
|
3750
4357
|
let val = v.getValue(...vox);
|
|
3751
|
-
return val;
|
|
4358
|
+
return { name: v.name, value: val, id: v.id, mm: mm, vox: vox };
|
|
3752
4359
|
}),
|
|
3753
4360
|
});
|
|
3754
4361
|
return;
|
|
3755
4362
|
}
|
|
3756
|
-
|
|
3757
|
-
this.scene.crosshairPos[0] = fracX;
|
|
3758
|
-
this.scene.crosshairPos[1] = fracY;
|
|
3759
|
-
}
|
|
3760
|
-
if (axCorSag === this.sliceTypeCoronal) {
|
|
3761
|
-
this.scene.crosshairPos[0] = fracX;
|
|
3762
|
-
this.scene.crosshairPos[2] = fracY;
|
|
3763
|
-
}
|
|
3764
|
-
if (axCorSag === this.sliceTypeSagittal) {
|
|
3765
|
-
this.scene.crosshairPos[1] = fracX;
|
|
3766
|
-
this.scene.crosshairPos[2] = fracY;
|
|
3767
|
-
}
|
|
4363
|
+
this.scene.crosshairPos = texFrac.slice();
|
|
3768
4364
|
if (this.opts.drawingEnabled) {
|
|
3769
4365
|
let pt = this.frac2vox(this.scene.crosshairPos);
|
|
4366
|
+
if (this.opts.penValue < 0 || Object.is(this.opts.penValue, -0)) {
|
|
4367
|
+
this.drawFloodFill(pt, Math.abs(this.opts.penValue));
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
3770
4370
|
if (isNaN(this.drawPenLocation[0])) {
|
|
3771
4371
|
this.drawPenAxCorSag = axCorSag;
|
|
3772
4372
|
this.drawPenFillPts = [];
|
|
3773
4373
|
this.drawPt(...pt, this.opts.penValue);
|
|
3774
|
-
this.drawUndoBitmap = this.drawBitmap.slice();
|
|
3775
4374
|
} else {
|
|
3776
4375
|
if (
|
|
3777
4376
|
pt[0] === this.drawPenLocation[0] &&
|
|
@@ -3779,22 +4378,23 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
|
3779
4378
|
pt[2] === this.drawPenLocation[2]
|
|
3780
4379
|
)
|
|
3781
4380
|
return;
|
|
3782
|
-
this.
|
|
4381
|
+
this.drawPenLine(pt, this.drawPenLocation, this.opts.penValue);
|
|
3783
4382
|
}
|
|
3784
4383
|
this.drawPenLocation = pt;
|
|
3785
4384
|
if (this.opts.isFilledPen) this.drawPenFillPts.push(pt);
|
|
3786
4385
|
this.refreshDrawing(false);
|
|
3787
4386
|
}
|
|
3788
4387
|
this.drawScene();
|
|
3789
|
-
this.
|
|
4388
|
+
this.opts.onLocationChange({
|
|
3790
4389
|
mm: this.frac2mm(this.scene.crosshairPos),
|
|
3791
4390
|
vox: this.frac2vox(this.scene.crosshairPos),
|
|
3792
4391
|
frac: this.scene.crosshairPos,
|
|
4392
|
+
xy: [x, y],
|
|
3793
4393
|
values: this.volumes.map((v) => {
|
|
3794
4394
|
let mm = this.frac2mm(this.scene.crosshairPos);
|
|
3795
4395
|
let vox = v.mm2vox(mm);
|
|
3796
4396
|
let val = v.getValue(...vox);
|
|
3797
|
-
return val;
|
|
4397
|
+
return { name: v.name, value: val, id: v.id, mm: mm, vox: vox };
|
|
3798
4398
|
}),
|
|
3799
4399
|
});
|
|
3800
4400
|
return;
|
|
@@ -3810,60 +4410,135 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
|
3810
4410
|
} //for i: each slice on screen
|
|
3811
4411
|
}; // mouseClick()
|
|
3812
4412
|
|
|
3813
|
-
|
|
3814
|
-
|
|
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);
|
|
3815
4437
|
this.lineShader.use(this.gl);
|
|
3816
4438
|
this.gl.uniform4fv(
|
|
3817
4439
|
this.lineShader.uniforms["lineColor"],
|
|
3818
|
-
this.opts.
|
|
4440
|
+
this.opts.rulerColor
|
|
3819
4441
|
);
|
|
3820
4442
|
this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
|
|
3821
4443
|
this.gl.canvas.width,
|
|
3822
4444
|
this.gl.canvas.height,
|
|
3823
4445
|
]);
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
leftTopWidthHeight[2],
|
|
3829
|
-
leftTopWidthHeight[3]
|
|
4446
|
+
//draw Line
|
|
4447
|
+
this.gl.uniform1f(
|
|
4448
|
+
this.lineShader.uniforms["thickness"],
|
|
4449
|
+
this.opts.rulerWidth
|
|
3830
4450
|
);
|
|
3831
|
-
this.gl.
|
|
4451
|
+
this.gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], startXYendXY);
|
|
3832
4452
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
3833
|
-
|
|
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
|
|
3834
4526
|
};
|
|
3835
4527
|
|
|
3836
4528
|
// not included in public docs
|
|
3837
|
-
Niivue.prototype.
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
// this.gl.uniform4f(this.lineShader.uniforms["leftTopWidthHeight"], ltwh[0], ltwh[1], ltwh[2], ltwh[3]);
|
|
3846
|
-
// this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
3847
|
-
// }
|
|
3848
|
-
this.colorbarShader.use(this.gl);
|
|
3849
|
-
this.gl.activeTexture(this.gl.TEXTURE1);
|
|
3850
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, this.colormapTexture);
|
|
3851
|
-
this.gl.texParameteri(
|
|
3852
|
-
this.gl.TEXTURE_2D,
|
|
3853
|
-
this.gl.TEXTURE_MIN_FILTER,
|
|
3854
|
-
this.gl.NEAREST
|
|
3855
|
-
);
|
|
3856
|
-
this.gl.texParameteri(
|
|
3857
|
-
this.gl.TEXTURE_2D,
|
|
3858
|
-
this.gl.TEXTURE_MAG_FILTER,
|
|
3859
|
-
this.gl.NEAREST
|
|
3860
|
-
);
|
|
3861
|
-
this.gl.uniform2fv(this.colorbarShader.uniforms["canvasWidthHeight"], [
|
|
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"], [
|
|
3862
4537
|
this.gl.canvas.width,
|
|
3863
4538
|
this.gl.canvas.height,
|
|
3864
4539
|
]);
|
|
3865
4540
|
this.gl.uniform4f(
|
|
3866
|
-
this.
|
|
4541
|
+
this.rectShader.uniforms["leftTopWidthHeight"],
|
|
3867
4542
|
leftTopWidthHeight[0],
|
|
3868
4543
|
leftTopWidthHeight[1],
|
|
3869
4544
|
leftTopWidthHeight[2],
|
|
@@ -3872,20 +4547,193 @@ Niivue.prototype.drawColorbar = function (leftTopWidthHeight) {
|
|
|
3872
4547
|
this.gl.bindVertexArray(this.genericVAO);
|
|
3873
4548
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
3874
4549
|
this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
|
|
3875
|
-
|
|
3876
|
-
this.gl.TEXTURE_2D,
|
|
3877
|
-
this.gl.TEXTURE_MIN_FILTER,
|
|
3878
|
-
this.gl.LINEAR
|
|
3879
|
-
);
|
|
3880
|
-
this.gl.texParameteri(
|
|
3881
|
-
this.gl.TEXTURE_2D,
|
|
3882
|
-
this.gl.TEXTURE_MAG_FILTER,
|
|
3883
|
-
this.gl.LINEAR
|
|
3884
|
-
);
|
|
3885
|
-
//gl.enable(gl.DEPTH_TEST);
|
|
3886
|
-
}; // drawColorbar()
|
|
4550
|
+
};
|
|
3887
4551
|
|
|
3888
|
-
|
|
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;
|
|
4615
|
+
if (leftTopWidthHeight[2] <= 0 || leftTopWidthHeight[3] <= 0) return;
|
|
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);
|
|
4649
|
+
this.colorbarShader.use(this.gl);
|
|
4650
|
+
this.gl.activeTexture(this.gl.TEXTURE1);
|
|
4651
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.colormapTexture);
|
|
4652
|
+
this.gl.texParameteri(
|
|
4653
|
+
this.gl.TEXTURE_2D,
|
|
4654
|
+
this.gl.TEXTURE_MIN_FILTER,
|
|
4655
|
+
this.gl.NEAREST
|
|
4656
|
+
);
|
|
4657
|
+
this.gl.texParameteri(
|
|
4658
|
+
this.gl.TEXTURE_2D,
|
|
4659
|
+
this.gl.TEXTURE_MAG_FILTER,
|
|
4660
|
+
this.gl.NEAREST
|
|
4661
|
+
);
|
|
4662
|
+
let lx = layer;
|
|
4663
|
+
if (isNegativeColor) lx += this.volumes.length;
|
|
4664
|
+
this.gl.uniform1f(this.colorbarShader.uniforms["layer"], lx);
|
|
4665
|
+
this.gl.uniform2fv(this.colorbarShader.uniforms["canvasWidthHeight"], [
|
|
4666
|
+
this.gl.canvas.width,
|
|
4667
|
+
this.gl.canvas.height,
|
|
4668
|
+
]);
|
|
4669
|
+
this.gl.uniform4fv(
|
|
4670
|
+
this.colorbarShader.uniforms["leftTopWidthHeight"],
|
|
4671
|
+
barLTWH
|
|
4672
|
+
);
|
|
4673
|
+
this.gl.bindVertexArray(this.genericVAO);
|
|
4674
|
+
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
4675
|
+
this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
|
|
4676
|
+
this.gl.texParameteri(
|
|
4677
|
+
this.gl.TEXTURE_2D,
|
|
4678
|
+
this.gl.TEXTURE_MIN_FILTER,
|
|
4679
|
+
this.gl.LINEAR
|
|
4680
|
+
);
|
|
4681
|
+
this.gl.texParameteri(
|
|
4682
|
+
this.gl.TEXTURE_2D,
|
|
4683
|
+
this.gl.TEXTURE_MAG_FILTER,
|
|
4684
|
+
this.gl.LINEAR
|
|
4685
|
+
);
|
|
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
|
+
};
|
|
4735
|
+
|
|
4736
|
+
// not included in public docs
|
|
3889
4737
|
Niivue.prototype.textWidth = function (scale, str) {
|
|
3890
4738
|
let w = 0;
|
|
3891
4739
|
var bytes = new TextEncoder().encode(str);
|
|
@@ -3965,7 +4813,48 @@ Niivue.prototype.drawTextLeft = function (xy, str, scale = 1, color = null) {
|
|
|
3965
4813
|
xy[0] -= this.textWidth(size, str);
|
|
3966
4814
|
xy[1] -= 0.5 * size;
|
|
3967
4815
|
this.drawText(xy, str, scale, color);
|
|
3968
|
-
}; //
|
|
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()
|
|
3969
4858
|
|
|
3970
4859
|
// not included in public docs
|
|
3971
4860
|
Niivue.prototype.drawTextBelow = function (xy, str, scale = 1, color = null) {
|
|
@@ -4001,170 +4890,382 @@ Niivue.prototype.setInterpolation = function (isNearest) {
|
|
|
4001
4890
|
this.drawScene();
|
|
4002
4891
|
}; // setInterpolation()
|
|
4003
4892
|
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
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
|
+
) {
|
|
4907
|
+
let gl = this.gl;
|
|
4908
|
+
gl.viewport(
|
|
4909
|
+
leftTopWidthHeight[0],
|
|
4910
|
+
this.gl.canvas.clientHeight -
|
|
4911
|
+
(leftTopWidthHeight[1] + leftTopWidthHeight[3]), //lower numbers near bottom
|
|
4912
|
+
leftTopWidthHeight[2],
|
|
4913
|
+
leftTopWidthHeight[3]
|
|
4914
|
+
);
|
|
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
|
|
4011
4926
|
let projectionMatrix = mat.mat4.create();
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
);
|
|
4024
|
-
//Wide window: "landscape" mode, height constrains
|
|
4025
|
-
else
|
|
4026
|
-
mat.mat4.ortho(
|
|
4027
|
-
projectionMatrix,
|
|
4028
|
-
-scale * whratio,
|
|
4029
|
-
scale * whratio,
|
|
4030
|
-
-scale,
|
|
4031
|
-
scale,
|
|
4032
|
-
0.01,
|
|
4033
|
-
scale * 8.0
|
|
4034
|
-
);
|
|
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);
|
|
4035
4938
|
const modelMatrix = mat.mat4.create();
|
|
4036
4939
|
modelMatrix[0] = -1; //mirror X coordinate
|
|
4037
4940
|
//push the model away from the camera so camera not inside model
|
|
4038
|
-
|
|
4039
4941
|
let translateVec3 = mat.vec3.fromValues(0, 0, -scale * 1.8); // to avoid clipping, >= SQRT(3)
|
|
4040
4942
|
mat.mat4.translate(modelMatrix, modelMatrix, translateVec3);
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
mat.mat4.
|
|
4045
|
-
-origin[0],
|
|
4046
|
-
-origin[1],
|
|
4047
|
-
-origin[2],
|
|
4048
|
-
]);
|
|
4049
|
-
//
|
|
4943
|
+
//apply elevation
|
|
4944
|
+
mat.mat4.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
|
|
4945
|
+
//apply azimuth
|
|
4946
|
+
mat.mat4.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
|
|
4050
4947
|
let iModelMatrix = mat.mat4.create();
|
|
4051
4948
|
mat.mat4.invert(iModelMatrix, modelMatrix);
|
|
4052
4949
|
let normalMatrix = mat.mat4.create();
|
|
4053
4950
|
mat.mat4.transpose(normalMatrix, iModelMatrix);
|
|
4054
|
-
//this.gl.uniformMatrix4fv(this.meshShader.uniforms["mvpMtx"], false, m);
|
|
4055
|
-
|
|
4056
|
-
// transpose(out, a) - > {mat4}
|
|
4057
|
-
// invert(out, a)
|
|
4058
|
-
// normalMatrix := modelMatrix.Inverse.Transpose;
|
|
4059
4951
|
let modelViewProjectionMatrix = mat.mat4.create();
|
|
4060
4952
|
mat.mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
|
|
4061
|
-
return [modelViewProjectionMatrix, modelMatrix, normalMatrix];
|
|
4062
|
-
}; // calculateMvpMatrix
|
|
4063
4953
|
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
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
|
|
4068
4978
|
) {
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
this.
|
|
4074
|
-
|
|
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
|
+
};
|
|
4994
|
+
|
|
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
|
+
};
|
|
5006
|
+
|
|
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";
|
|
5013
|
+
|
|
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;
|
|
5024
|
+
}
|
|
5025
|
+
this.drawTextBelow(
|
|
5026
|
+
[
|
|
5027
|
+
leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5,
|
|
5028
|
+
leftTopWidthHeight[1],
|
|
5029
|
+
],
|
|
5030
|
+
topText
|
|
4075
5031
|
);
|
|
4076
5032
|
|
|
4077
|
-
|
|
4078
|
-
|
|
5033
|
+
this.drawTextRight(
|
|
5034
|
+
[
|
|
5035
|
+
leftTopWidthHeight[0],
|
|
5036
|
+
leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5,
|
|
5037
|
+
],
|
|
5038
|
+
leftText
|
|
5039
|
+
);
|
|
5040
|
+
};
|
|
4079
5041
|
|
|
4080
|
-
|
|
4081
|
-
|
|
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
|
+
};
|
|
4082
5074
|
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
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;
|
|
4086
5094
|
} else {
|
|
4087
|
-
|
|
4088
|
-
|
|
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];
|
|
4089
5122
|
}
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
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
|
|
4112
5145
|
);
|
|
4113
|
-
gl.bindVertexArray(this.unusedVAO);
|
|
4114
|
-
}
|
|
4115
|
-
//draw fibers
|
|
4116
|
-
if (!hasFibers) {
|
|
4117
|
-
gl.enable(gl.BLEND);
|
|
4118
|
-
gl.depthFunc(gl.ALWAYS);
|
|
4119
5146
|
return;
|
|
4120
5147
|
}
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
gl.
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
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
|
|
4134
5209
|
);
|
|
4135
|
-
gl.bindVertexArray(this.unusedVAO);
|
|
4136
5210
|
}
|
|
4137
|
-
|
|
4138
|
-
|
|
5211
|
+
|
|
5212
|
+
if (isNaN(customMM))
|
|
5213
|
+
//no crossbars for mosaic view
|
|
5214
|
+
this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix);
|
|
5215
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag);
|
|
5216
|
+
//gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
4139
5217
|
this.readyForSync = true;
|
|
4140
|
-
}; //
|
|
5218
|
+
}; // draw2DMM()
|
|
4141
5219
|
|
|
4142
5220
|
// not included in public docs
|
|
4143
|
-
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
|
+
}
|
|
4144
5231
|
this.gl.cullFace(this.gl.FRONT);
|
|
5232
|
+
let ltwh = leftTopWidthHeight.slice();
|
|
4145
5233
|
let gl = this.gl;
|
|
4146
5234
|
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
5235
|
+
//gl.viewport(ltwh[0], ltwh[1], ltwh[2], ltwh[3]);
|
|
4147
5236
|
gl.disable(gl.DEPTH_TEST);
|
|
4148
|
-
|
|
4149
|
-
|
|
5237
|
+
let sliceDim = 2;
|
|
5238
|
+
let crossXYZ = [
|
|
4150
5239
|
this.scene.crosshairPos[0],
|
|
4151
5240
|
this.scene.crosshairPos[1],
|
|
4152
5241
|
this.scene.crosshairPos[2],
|
|
4153
5242
|
]; //axial: width=i, height=j, slice=k
|
|
4154
|
-
if (axCorSag === this.sliceTypeCoronal)
|
|
5243
|
+
if (axCorSag === this.sliceTypeCoronal) {
|
|
5244
|
+
sliceDim = 1;
|
|
4155
5245
|
crossXYZ = [
|
|
4156
5246
|
this.scene.crosshairPos[0],
|
|
4157
5247
|
this.scene.crosshairPos[2],
|
|
4158
5248
|
this.scene.crosshairPos[1],
|
|
4159
5249
|
]; //coronal: width=i, height=k, slice=j
|
|
4160
|
-
|
|
5250
|
+
}
|
|
5251
|
+
if (axCorSag === this.sliceTypeSagittal) {
|
|
5252
|
+
sliceDim = 0;
|
|
4161
5253
|
crossXYZ = [
|
|
4162
5254
|
this.scene.crosshairPos[1],
|
|
4163
5255
|
this.scene.crosshairPos[2],
|
|
4164
5256
|
this.scene.crosshairPos[0],
|
|
4165
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
|
+
}
|
|
4166
5265
|
let isMirrorLR =
|
|
4167
5266
|
this.opts.isRadiologicalConvention && axCorSag < this.sliceTypeSagittal;
|
|
5267
|
+
if (this.opts.sagittalNoseLeft && axCorSag === this.sliceTypeSagittal)
|
|
5268
|
+
isMirrorLR = !isMirrorLR;
|
|
4168
5269
|
this.sliceShader.use(this.gl);
|
|
4169
5270
|
this.gl.uniform1f(
|
|
4170
5271
|
this.sliceShader.uniforms["opacity"],
|
|
@@ -4192,29 +5293,35 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
|
|
|
4192
5293
|
leftTopWidthHeight[2],
|
|
4193
5294
|
leftTopWidthHeight[3]
|
|
4194
5295
|
);
|
|
4195
|
-
//gl.uniform4f(sliceShader.uniforms["leftTopWidthHeight"], leftTopWidthHeight[0], leftTopWidthHeight[1], leftTopWidthHeight[2], leftTopWidthHeight[3]);
|
|
4196
|
-
//gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
4197
5296
|
this.gl.bindVertexArray(this.genericVAO); //set vertex attributes
|
|
4198
5297
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
4199
5298
|
//record screenSlices to detect mouse click positions
|
|
4200
|
-
this.screenSlices
|
|
4201
|
-
leftTopWidthHeight
|
|
4202
|
-
|
|
4203
|
-
|
|
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;
|
|
4204
5310
|
if (this.opts.crosshairWidth <= 0.0) return;
|
|
4205
|
-
this.
|
|
5311
|
+
this.gl.bindVertexArray(this.genericVAO);
|
|
5312
|
+
this.rectShader.use(this.gl);
|
|
4206
5313
|
this.gl.uniform4fv(
|
|
4207
|
-
this.
|
|
5314
|
+
this.rectShader.uniforms["lineColor"],
|
|
4208
5315
|
this.opts.crosshairColor
|
|
4209
5316
|
);
|
|
4210
|
-
this.gl.uniform2fv(this.
|
|
5317
|
+
this.gl.uniform2fv(this.rectShader.uniforms["canvasWidthHeight"], [
|
|
4211
5318
|
this.gl.canvas.width,
|
|
4212
5319
|
this.gl.canvas.height,
|
|
4213
5320
|
]);
|
|
4214
5321
|
//vertical line of crosshair:
|
|
4215
5322
|
var xleft = leftTopWidthHeight[0] + leftTopWidthHeight[2] * crossXYZ[0];
|
|
4216
5323
|
this.gl.uniform4f(
|
|
4217
|
-
this.
|
|
5324
|
+
this.rectShader.uniforms["leftTopWidthHeight"],
|
|
4218
5325
|
xleft - 0.5 * this.opts.crosshairWidth,
|
|
4219
5326
|
leftTopWidthHeight[1],
|
|
4220
5327
|
this.opts.crosshairWidth,
|
|
@@ -4225,7 +5332,7 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
|
|
|
4225
5332
|
var xtop =
|
|
4226
5333
|
leftTopWidthHeight[1] + leftTopWidthHeight[3] * (1.0 - crossXYZ[1]);
|
|
4227
5334
|
this.gl.uniform4f(
|
|
4228
|
-
this.
|
|
5335
|
+
this.rectShader.uniforms["leftTopWidthHeight"],
|
|
4229
5336
|
leftTopWidthHeight[0],
|
|
4230
5337
|
xtop - 0.5 * this.opts.crosshairWidth,
|
|
4231
5338
|
leftTopWidthHeight[2],
|
|
@@ -4233,60 +5340,34 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
|
|
|
4233
5340
|
);
|
|
4234
5341
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
4235
5342
|
this.gl.bindVertexArray(this.unusedVAO); //set vertex attributes
|
|
4236
|
-
|
|
4237
5343
|
this.gl.enable(this.gl.CULL_FACE);
|
|
4238
|
-
if (isMirrorLR)
|
|
4239
|
-
this.drawTextRight(
|
|
4240
|
-
[
|
|
4241
|
-
leftTopWidthHeight[0] + leftTopWidthHeight[2] + 1,
|
|
4242
|
-
leftTopWidthHeight[1] + 0.5 * leftTopWidthHeight[3],
|
|
4243
|
-
],
|
|
4244
|
-
"R"
|
|
4245
|
-
);
|
|
4246
|
-
else if (axCorSag < this.sliceTypeSagittal)
|
|
4247
|
-
this.drawTextRight(
|
|
4248
|
-
[
|
|
4249
|
-
leftTopWidthHeight[0] + 1,
|
|
4250
|
-
leftTopWidthHeight[1] + 0.5 * leftTopWidthHeight[3],
|
|
4251
|
-
],
|
|
4252
|
-
"L"
|
|
4253
|
-
);
|
|
4254
|
-
if (axCorSag === this.sliceTypeAxial)
|
|
4255
|
-
this.drawTextBelow(
|
|
4256
|
-
[
|
|
4257
|
-
leftTopWidthHeight[0] + 0.5 * leftTopWidthHeight[2],
|
|
4258
|
-
leftTopWidthHeight[1] + 1,
|
|
4259
|
-
],
|
|
4260
|
-
"A"
|
|
4261
|
-
);
|
|
4262
|
-
if (axCorSag > this.sliceTypeAxial)
|
|
4263
|
-
this.drawTextBelow(
|
|
4264
|
-
[
|
|
4265
|
-
leftTopWidthHeight[0] + 0.5 * leftTopWidthHeight[2],
|
|
4266
|
-
leftTopWidthHeight[1] + 1,
|
|
4267
|
-
],
|
|
4268
|
-
"S"
|
|
4269
|
-
);
|
|
4270
|
-
if (axCorSag !== this.sliceTypeAxial) return;
|
|
4271
|
-
// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
4272
|
-
this.setPivot3D();
|
|
4273
|
-
gl.viewport(
|
|
4274
|
-
leftTopWidthHeight[0],
|
|
4275
|
-
leftTopWidthHeight[1],
|
|
4276
|
-
leftTopWidthHeight[2],
|
|
4277
|
-
leftTopWidthHeight[3]
|
|
4278
|
-
);
|
|
4279
5344
|
gl.enable(gl.DEPTH_TEST);
|
|
4280
5345
|
gl.depthFunc(gl.ALWAYS);
|
|
4281
5346
|
gl.clearDepth(0.0);
|
|
4282
5347
|
this.sync();
|
|
4283
|
-
}; //
|
|
5348
|
+
}; // draw2DVox()
|
|
4284
5349
|
|
|
4285
|
-
Niivue.prototype.
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
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;
|
|
4290
5371
|
//pivot from center of objects
|
|
4291
5372
|
//let scale = this.furthestVertexFromOrigin;
|
|
4292
5373
|
//let origin = [0,0,0];
|
|
@@ -4325,26 +5406,9 @@ Niivue.prototype.calculateMvpMatrix = function () {
|
|
|
4325
5406
|
if (this.position)
|
|
4326
5407
|
mat.mat4.translate(modelMatrix, modelMatrix, this.position);
|
|
4327
5408
|
//apply elevation
|
|
4328
|
-
mat.mat4.rotateX(
|
|
4329
|
-
modelMatrix,
|
|
4330
|
-
modelMatrix,
|
|
4331
|
-
deg2rad(270 - this.scene.renderElevation)
|
|
4332
|
-
);
|
|
5409
|
+
mat.mat4.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
|
|
4333
5410
|
//apply azimuth
|
|
4334
|
-
mat.mat4.rotateZ(
|
|
4335
|
-
modelMatrix,
|
|
4336
|
-
modelMatrix,
|
|
4337
|
-
deg2rad(this.scene.renderAzimuth - 180)
|
|
4338
|
-
);
|
|
4339
|
-
//translate object to be in center of field of view (e.g. CT brain scans where origin is distant table center)
|
|
4340
|
-
/*if (this.volumeObject3D) {
|
|
4341
|
-
mat.mat4.translate(
|
|
4342
|
-
modelMatrix,
|
|
4343
|
-
modelMatrix,
|
|
4344
|
-
this.volumeObject3D.originNegate
|
|
4345
|
-
);
|
|
4346
|
-
}*/
|
|
4347
|
-
|
|
5411
|
+
mat.mat4.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
|
|
4348
5412
|
mat.mat4.translate(modelMatrix, modelMatrix, [
|
|
4349
5413
|
-origin[0],
|
|
4350
5414
|
-origin[1],
|
|
@@ -4366,7 +5430,7 @@ Niivue.prototype.calculateMvpMatrix = function () {
|
|
|
4366
5430
|
}; // calculateMvpMatrix
|
|
4367
5431
|
|
|
4368
5432
|
// not included in public docs
|
|
4369
|
-
Niivue.prototype.calculateRayDirection = function () {
|
|
5433
|
+
Niivue.prototype.calculateRayDirection = function (azimuth, elevation) {
|
|
4370
5434
|
function deg2rad(deg) {
|
|
4371
5435
|
return deg * (Math.PI / 180.0);
|
|
4372
5436
|
}
|
|
@@ -4374,17 +5438,9 @@ Niivue.prototype.calculateRayDirection = function () {
|
|
|
4374
5438
|
modelMatrix[0] = -1; //mirror X coordinate
|
|
4375
5439
|
//push the model away from the camera so camera not inside model
|
|
4376
5440
|
//apply elevation
|
|
4377
|
-
mat.mat4.rotateX(
|
|
4378
|
-
modelMatrix,
|
|
4379
|
-
modelMatrix,
|
|
4380
|
-
deg2rad(270 - this.scene.renderElevation)
|
|
4381
|
-
);
|
|
5441
|
+
mat.mat4.rotateX(modelMatrix, modelMatrix, deg2rad(270 - elevation));
|
|
4382
5442
|
//apply azimuth
|
|
4383
|
-
mat.mat4.rotateZ(
|
|
4384
|
-
modelMatrix,
|
|
4385
|
-
modelMatrix,
|
|
4386
|
-
deg2rad(this.scene.renderAzimuth - 180)
|
|
4387
|
-
);
|
|
5443
|
+
mat.mat4.rotateZ(modelMatrix, modelMatrix, deg2rad(azimuth - 180));
|
|
4388
5444
|
if (this.back.obliqueRAS) {
|
|
4389
5445
|
let oblique = mat.mat4.clone(this.back.obliqueRAS);
|
|
4390
5446
|
mat.mat4.multiply(modelMatrix, modelMatrix, oblique);
|
|
@@ -4471,6 +5527,8 @@ Niivue.prototype.setPivot3D = function () {
|
|
|
4471
5527
|
this.pivot3D = [pivot[0], pivot[1], pivot[2]];
|
|
4472
5528
|
//find scale of scene
|
|
4473
5529
|
mat.vec3.subtract(pivot, mx, mn);
|
|
5530
|
+
this.extentsMin = mn;
|
|
5531
|
+
this.extentsMax = mx;
|
|
4474
5532
|
this.furthestFromPivot = mat.vec3.length(pivot) * 0.5; //pivot is half way between the extreme vertices
|
|
4475
5533
|
}; // setPivot3D()
|
|
4476
5534
|
|
|
@@ -4499,14 +5557,14 @@ Niivue.prototype.drawGraphLine = function (
|
|
|
4499
5557
|
};
|
|
4500
5558
|
|
|
4501
5559
|
Niivue.prototype.drawRect = function (LTWH, color = [1, 0, 0, 0.5]) {
|
|
4502
|
-
this.
|
|
4503
|
-
this.gl.uniform4fv(this.
|
|
4504
|
-
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"], [
|
|
4505
5563
|
this.gl.canvas.width,
|
|
4506
5564
|
this.gl.canvas.height,
|
|
4507
5565
|
]);
|
|
4508
5566
|
this.gl.uniform4f(
|
|
4509
|
-
this.
|
|
5567
|
+
this.rectShader.uniforms["leftTopWidthHeight"],
|
|
4510
5568
|
LTWH[0],
|
|
4511
5569
|
LTWH[1],
|
|
4512
5570
|
LTWH[2],
|
|
@@ -4517,21 +5575,6 @@ Niivue.prototype.drawRect = function (LTWH, color = [1, 0, 0, 0.5]) {
|
|
|
4517
5575
|
this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
|
|
4518
5576
|
};
|
|
4519
5577
|
|
|
4520
|
-
function tickSpacing(tickCount, mn, mx) {
|
|
4521
|
-
//https://www.realtimerendering.com/resources/GraphicsGems/gems/Label.c
|
|
4522
|
-
//https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
|
|
4523
|
-
let range = Math.abs(mx - mn);
|
|
4524
|
-
if (range === 0.0) return [0, 0];
|
|
4525
|
-
let unroundedTickSize = range / (tickCount - 1);
|
|
4526
|
-
let x = Math.ceil(Math.log10(unroundedTickSize) - 1);
|
|
4527
|
-
let pow10x = Math.pow(10, x);
|
|
4528
|
-
let spacing = Math.ceil(unroundedTickSize / pow10x) * pow10x;
|
|
4529
|
-
let ticMin = mn;
|
|
4530
|
-
if ((mn / spacing) % 1 !== 0.0)
|
|
4531
|
-
ticMin = Math.sign(ticMin) * Math.round(Math.abs(ticMin));
|
|
4532
|
-
return [spacing, ticMin];
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
5578
|
Niivue.prototype.drawGraph = function () {
|
|
4536
5579
|
let gl = this.gl;
|
|
4537
5580
|
let graph = this.graph;
|
|
@@ -4539,7 +5582,7 @@ Niivue.prototype.drawGraph = function () {
|
|
|
4539
5582
|
this.graph.autoSizeMultiplanar &&
|
|
4540
5583
|
this.sliceType === this.sliceTypeMultiplanar
|
|
4541
5584
|
) {
|
|
4542
|
-
for (let i = 0; i < this.
|
|
5585
|
+
for (let i = 0; i < this.screenSlices.length; i++) {
|
|
4543
5586
|
var axCorSag = this.screenSlices[i].axCorSag;
|
|
4544
5587
|
if (axCorSag !== this.sliceTypeSagittal) continue;
|
|
4545
5588
|
var ltwh = this.screenSlices[i].leftTopWidthHeight;
|
|
@@ -4629,7 +5672,7 @@ Niivue.prototype.drawGraph = function () {
|
|
|
4629
5672
|
graph.backColor[3],
|
|
4630
5673
|
];
|
|
4631
5674
|
this.drawRect(graph.LTWH, borderColor);
|
|
4632
|
-
let [spacing, ticMin] = tickSpacing(
|
|
5675
|
+
let [spacing, ticMin] = tickSpacing(mn, mx);
|
|
4633
5676
|
//determine font size
|
|
4634
5677
|
function humanize(x) {
|
|
4635
5678
|
//drop trailing zeros from numerical string
|
|
@@ -4774,27 +5817,78 @@ Niivue.prototype.drawGraph = function () {
|
|
|
4774
5817
|
}
|
|
4775
5818
|
};
|
|
4776
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
|
+
|
|
4777
5827
|
// not included in public docs
|
|
4778
|
-
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;
|
|
4779
5837
|
this.setPivot3D();
|
|
5838
|
+
if (!isMosaic) {
|
|
5839
|
+
azimuth = this.scene.renderAzimuth;
|
|
5840
|
+
elevation = this.scene.renderElevation;
|
|
5841
|
+
}
|
|
4780
5842
|
let gl = this.gl;
|
|
4781
|
-
|
|
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
|
+
);
|
|
4782
5879
|
gl.enable(gl.DEPTH_TEST);
|
|
4783
5880
|
gl.depthFunc(gl.ALWAYS);
|
|
4784
5881
|
gl.clearDepth(0.0);
|
|
5882
|
+
|
|
4785
5883
|
if (this.volumes.length === 0) {
|
|
4786
5884
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
4787
|
-
this.drawMesh3D(true, 1);
|
|
5885
|
+
this.drawMesh3D(true, 1, mvpMatrix, modelMatrix, normalMatrix);
|
|
5886
|
+
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
4788
5887
|
return;
|
|
4789
5888
|
}
|
|
4790
|
-
|
|
4791
5889
|
// mvp matrix and ray direction can now be a constant because of world space
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
[mvpMatrix, modelMatrix, normalMatrix] = this.calculateMvpMatrix(
|
|
4795
|
-
this.volumeObject3D
|
|
4796
|
-
);
|
|
4797
|
-
const rayDir = this.calculateRayDirection();
|
|
5890
|
+
const rayDir = this.calculateRayDirection(azimuth, elevation);
|
|
5891
|
+
|
|
4798
5892
|
let object3D = this.volumeObject3D;
|
|
4799
5893
|
// render picking surfaces
|
|
4800
5894
|
if (this.scene.mouseDepthPicker) {
|
|
@@ -4815,10 +5909,11 @@ Niivue.prototype.draw3D = function () {
|
|
|
4815
5909
|
gl.bindVertexArray(this.unusedVAO);
|
|
4816
5910
|
}
|
|
4817
5911
|
//check if we have a selected object
|
|
4818
|
-
const pixelX =
|
|
5912
|
+
const pixelX =
|
|
5913
|
+
(this.mousePos[0] * leftTopWidthHeight[2]) / leftTopWidthHeight[2];
|
|
4819
5914
|
const pixelY =
|
|
4820
5915
|
gl.canvas.height -
|
|
4821
|
-
(this.mousePos[1] *
|
|
5916
|
+
(this.mousePos[1] * leftTopWidthHeight[3]) / leftTopWidthHeight[3] -
|
|
4822
5917
|
1;
|
|
4823
5918
|
const rgbaPixel = new Uint8Array(4);
|
|
4824
5919
|
gl.readPixels(
|
|
@@ -4860,11 +5955,12 @@ Niivue.prototype.draw3D = function () {
|
|
|
4860
5955
|
gl.drawElements(object3D.mode, object3D.indexCount, gl.UNSIGNED_SHORT, 0);
|
|
4861
5956
|
gl.bindVertexArray(this.unusedVAO);
|
|
4862
5957
|
}
|
|
4863
|
-
this.drawCrosshairs3D(true, 1.0);
|
|
4864
|
-
this.drawMesh3D(true, 1.0);
|
|
4865
|
-
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);
|
|
4866
5961
|
// this.drawMesh3D(true, 1.0);
|
|
4867
|
-
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);
|
|
4868
5964
|
let posString =
|
|
4869
5965
|
"azimuth: " +
|
|
4870
5966
|
this.scene.renderAzimuth.toFixed(0) +
|
|
@@ -4877,12 +5973,22 @@ Niivue.prototype.draw3D = function () {
|
|
|
4877
5973
|
return posString;
|
|
4878
5974
|
}; // draw3D()
|
|
4879
5975
|
|
|
4880
|
-
Niivue.prototype.drawMesh3D = function (
|
|
5976
|
+
Niivue.prototype.drawMesh3D = function (
|
|
5977
|
+
isDepthTest = true,
|
|
5978
|
+
alpha = 1.0,
|
|
5979
|
+
m,
|
|
5980
|
+
modelMtx,
|
|
5981
|
+
normMtx
|
|
5982
|
+
) {
|
|
4881
5983
|
if (this.meshes.length < 1) return;
|
|
4882
5984
|
let gl = this.gl;
|
|
4883
|
-
let m, modelMtx, normMtx;
|
|
4884
|
-
|
|
4885
|
-
|
|
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
|
+
);
|
|
4886
5992
|
gl.enable(gl.DEPTH_TEST);
|
|
4887
5993
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
4888
5994
|
|
|
@@ -4948,7 +6054,11 @@ Niivue.prototype.drawMesh3D = function (isDepthTest = true, alpha = 1.0) {
|
|
|
4948
6054
|
this.readyForSync = true;
|
|
4949
6055
|
}; //drawMesh3D()
|
|
4950
6056
|
|
|
4951
|
-
Niivue.prototype.drawCrosshairs3D = function (
|
|
6057
|
+
Niivue.prototype.drawCrosshairs3D = function (
|
|
6058
|
+
isDepthTest = true,
|
|
6059
|
+
alpha = 1.0,
|
|
6060
|
+
mvpMtx = null
|
|
6061
|
+
) {
|
|
4952
6062
|
if (!this.opts.show3Dcrosshair) return;
|
|
4953
6063
|
let gl = this.gl;
|
|
4954
6064
|
let mm = this.frac2mm(this.scene.crosshairPos);
|
|
@@ -4964,10 +6074,12 @@ Niivue.prototype.drawCrosshairs3D = function (isDepthTest = true, alpha = 1.0) {
|
|
|
4964
6074
|
gl.deleteBuffer(this.crosshairs3D.indexBuffer); //TODO: handle in nvimage.js: create once, update with bufferSubData
|
|
4965
6075
|
gl.deleteBuffer(this.crosshairs3D.vertexBuffer); //TODO: handle in nvimage.js: create once, update with bufferSubData
|
|
4966
6076
|
}
|
|
4967
|
-
let radius =
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
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
|
+
);
|
|
4971
6083
|
this.crosshairs3D = NiivueObject3D.generateCrosshairs(
|
|
4972
6084
|
this.gl,
|
|
4973
6085
|
1,
|
|
@@ -4983,13 +6095,17 @@ Niivue.prototype.drawCrosshairs3D = function (isDepthTest = true, alpha = 1.0) {
|
|
|
4983
6095
|
}
|
|
4984
6096
|
let crosshairsShader = this.surfaceShader;
|
|
4985
6097
|
crosshairsShader.use(this.gl);
|
|
4986
|
-
let
|
|
6098
|
+
let modelMtx, normMtx;
|
|
4987
6099
|
// eslint-disable-next-line no-unused-vars
|
|
4988
|
-
|
|
4989
|
-
|
|
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);
|
|
4990
6107
|
|
|
4991
6108
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.crosshairs3D.indexBuffer);
|
|
4992
|
-
|
|
4993
6109
|
gl.enable(gl.DEPTH_TEST);
|
|
4994
6110
|
let color = [...this.opts.crosshairColor];
|
|
4995
6111
|
if (isDepthTest) {
|
|
@@ -5001,6 +6117,7 @@ Niivue.prototype.drawCrosshairs3D = function (isDepthTest = true, alpha = 1.0) {
|
|
|
5001
6117
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
5002
6118
|
gl.depthFunc(gl.ALWAYS);
|
|
5003
6119
|
}
|
|
6120
|
+
|
|
5004
6121
|
color[3] = alpha;
|
|
5005
6122
|
gl.uniform4fv(crosshairsShader.uniforms["surfaceColor"], color);
|
|
5006
6123
|
gl.bindVertexArray(this.crosshairs3D.vao);
|
|
@@ -5068,94 +6185,92 @@ Niivue.prototype.moveCrosshairInVox = function (x, y, z) {
|
|
|
5068
6185
|
this.drawScene();
|
|
5069
6186
|
};
|
|
5070
6187
|
|
|
5071
|
-
// not included in public docs
|
|
5072
6188
|
Niivue.prototype.frac2mm = function (frac, volIdx = 0) {
|
|
5073
|
-
//convert from normalized texture space XYZ= [0..1, 0..1 ,0..1] to object space in millimeters
|
|
5074
6189
|
let pos = mat.vec4.fromValues(frac[0], frac[1], frac[2], 1);
|
|
5075
|
-
|
|
5076
|
-
let dim = mat.vec4.fromValues(
|
|
5077
|
-
this.volumes[volIdx].dimsRAS[1],
|
|
5078
|
-
this.volumes[volIdx].dimsRAS[2],
|
|
5079
|
-
this.volumes[volIdx].dimsRAS[3],
|
|
5080
|
-
1
|
|
5081
|
-
);
|
|
5082
|
-
let sform = mat.mat4.clone(this.volumes[volIdx].matRAS);
|
|
5083
|
-
mat.mat4.transpose(sform, sform);
|
|
5084
|
-
mat.vec4.mul(pos, pos, dim);
|
|
5085
|
-
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
|
|
5086
|
-
mat.vec4.add(pos, pos, shim);
|
|
5087
|
-
mat.vec4.transformMat4(pos, pos, sform);
|
|
6190
|
+
mat.vec4.transformMat4(pos, pos, this.volumes[volIdx].frac2mm);
|
|
5088
6191
|
return pos;
|
|
5089
|
-
};
|
|
5090
|
-
|
|
5091
|
-
// not included in public docs
|
|
5092
|
-
Niivue.prototype.canvasPos2frac = function (canvasPos) {
|
|
5093
|
-
let x, y, z;
|
|
5094
|
-
|
|
5095
|
-
// convert canvas pos to normalized texture space
|
|
5096
|
-
for (let i = 0; i < this.numScreenSlices; i++) {
|
|
5097
|
-
var axCorSag = this.screenSlices[i].axCorSag;
|
|
5098
|
-
if (axCorSag > this.sliceTypeSagittal) continue;
|
|
6192
|
+
};
|
|
5099
6193
|
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
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;
|
|
5106
6240
|
}
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
fracX = 1.0 - fracX;
|
|
6241
|
+
if (axCorSag === this.sliceTypeCoronal) {
|
|
6242
|
+
texFrac[0] = fracX;
|
|
6243
|
+
texFrac[2] = fracY;
|
|
5111
6244
|
}
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
// this is the slice the user right clicked in
|
|
5116
|
-
switch (axCorSag) {
|
|
5117
|
-
case this.sliceTypeAxial:
|
|
5118
|
-
break;
|
|
5119
|
-
case this.sliceTypeCoronal:
|
|
5120
|
-
break;
|
|
5121
|
-
default:
|
|
5122
|
-
}
|
|
5123
|
-
|
|
5124
|
-
if (axCorSag === this.sliceTypeAxial) {
|
|
5125
|
-
x = fracX;
|
|
5126
|
-
y = fracY;
|
|
5127
|
-
z = this.scene.crosshairPos[2];
|
|
5128
|
-
}
|
|
5129
|
-
if (axCorSag === this.sliceTypeCoronal) {
|
|
5130
|
-
x = fracX;
|
|
5131
|
-
y = this.scene.crosshairPos[1];
|
|
5132
|
-
z = fracY;
|
|
5133
|
-
}
|
|
5134
|
-
if (axCorSag === this.sliceTypeSagittal) {
|
|
5135
|
-
x = this.scene.crosshairPos[0];
|
|
5136
|
-
y = fracX;
|
|
5137
|
-
z = fracY;
|
|
5138
|
-
}
|
|
5139
|
-
|
|
5140
|
-
break;
|
|
6245
|
+
if (axCorSag === this.sliceTypeSagittal) {
|
|
6246
|
+
texFrac[1] = fracX;
|
|
6247
|
+
texFrac[2] = fracY;
|
|
5141
6248
|
}
|
|
5142
6249
|
}
|
|
6250
|
+
return texFrac;
|
|
6251
|
+
}; // screenXY2TextureFrac()
|
|
5143
6252
|
|
|
5144
|
-
|
|
5145
|
-
|
|
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
|
+
};
|
|
5146
6261
|
|
|
5147
6262
|
// not included in public docs
|
|
5148
6263
|
// note: we also have a "sliceScale" method, which could be confusing
|
|
5149
6264
|
Niivue.prototype.scaleSlice = function (w, h) {
|
|
5150
6265
|
let scalePix = this.gl.canvas.clientWidth / w;
|
|
5151
|
-
if (h * scalePix > this.
|
|
5152
|
-
scalePix = this.
|
|
6266
|
+
if (h * scalePix > this.effectiveCanvasHeight())
|
|
6267
|
+
scalePix = this.effectiveCanvasHeight() / h;
|
|
5153
6268
|
//canvas space is 0,0...w,h with origin at upper left
|
|
5154
6269
|
let wPix = w * scalePix;
|
|
5155
6270
|
let hPix = h * scalePix;
|
|
5156
6271
|
let leftTopWidthHeight = [
|
|
5157
6272
|
(this.gl.canvas.clientWidth - wPix) * 0.5,
|
|
5158
|
-
(this.
|
|
6273
|
+
(this.effectiveCanvasHeight() - hPix) * 0.5,
|
|
5159
6274
|
wPix,
|
|
5160
6275
|
hPix,
|
|
5161
6276
|
];
|
|
@@ -5183,11 +6298,365 @@ Niivue.prototype.drawThumbnail = function () {
|
|
|
5183
6298
|
this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
|
|
5184
6299
|
};
|
|
5185
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
|
+
|
|
5186
6654
|
// not included in public docs
|
|
5187
6655
|
Niivue.prototype.drawScene = function () {
|
|
5188
6656
|
if (!this.initialized) {
|
|
5189
6657
|
return; // do not do anything until we are initialized (init will call drawScene).
|
|
5190
6658
|
}
|
|
6659
|
+
this.colorbarHeight = 0;
|
|
5191
6660
|
this.gl.clearColor(
|
|
5192
6661
|
this.opts.backColor[0],
|
|
5193
6662
|
this.opts.backColor[1],
|
|
@@ -5206,6 +6675,7 @@ Niivue.prototype.drawScene = function () {
|
|
|
5206
6675
|
typeof this.volumes[0].dims === "undefined"
|
|
5207
6676
|
) {
|
|
5208
6677
|
if (this.meshes.length > 0) {
|
|
6678
|
+
this.screenSlices = []; // empty array
|
|
5209
6679
|
this.sliceType = this.sliceTypeRender; //only meshes loaded: we must use 3D render mode
|
|
5210
6680
|
return this.draw3D(); //meshes loaded but no volume
|
|
5211
6681
|
}
|
|
@@ -5213,95 +6683,122 @@ Niivue.prototype.drawScene = function () {
|
|
|
5213
6683
|
return;
|
|
5214
6684
|
}
|
|
5215
6685
|
if (!this.back.hasOwnProperty("dims")) return;
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
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);
|
|
5236
6708
|
}
|
|
5237
|
-
|
|
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;
|
|
5238
6718
|
}
|
|
5239
|
-
|
|
5240
|
-
this.
|
|
5241
|
-
|
|
5242
|
-
if (this.sliceType === this.sliceTypeAxial) {
|
|
5243
|
-
//draw axial
|
|
5244
|
-
let leftTopWidthHeight = this.scaleSlice(volScale[0], volScale[1]);
|
|
5245
|
-
this.draw2D(leftTopWidthHeight, 0);
|
|
5246
|
-
} else if (this.sliceType === this.sliceTypeCoronal) {
|
|
5247
|
-
//draw coronal
|
|
5248
|
-
let leftTopWidthHeight = this.scaleSlice(volScale[0], volScale[2]);
|
|
5249
|
-
this.draw2D(leftTopWidthHeight, 1);
|
|
5250
|
-
} else if (this.sliceType === this.sliceTypeSagittal) {
|
|
5251
|
-
//draw sagittal
|
|
5252
|
-
let leftTopWidthHeight = this.scaleSlice(volScale[1], volScale[2]);
|
|
5253
|
-
this.draw2D(leftTopWidthHeight, 2);
|
|
6719
|
+
if (this.opts.isColorbar) this.reserveColorbarPanel();
|
|
6720
|
+
if (this.sliceMosaicString.length > 0) {
|
|
6721
|
+
this.drawMosaic(this.sliceMosaicString);
|
|
5254
6722
|
} else {
|
|
5255
|
-
//
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
);
|
|
5265
|
-
let wX1 =
|
|
5266
|
-
(ltwh3x1[2] * volScale[0]) / (volScale[0] + volScale[0] + volScale[1]);
|
|
5267
|
-
if (wX1 > wX && !this.graph.autoSizeMultiplanar) {
|
|
5268
|
-
//landscape screen ratio: 3 slices in single row
|
|
5269
|
-
let pixScale = wX1 / volScale[0];
|
|
5270
|
-
let hY1 = volScale[1] * pixScale;
|
|
5271
|
-
let hZ1 = volScale[2] * pixScale;
|
|
5272
|
-
//draw axial
|
|
5273
|
-
this.draw2D([ltwh3x1[0], ltwh3x1[1], wX1, hY1], 0);
|
|
5274
|
-
//draw coronal
|
|
5275
|
-
this.draw2D([ltwh3x1[0] + wX1, ltwh3x1[1], wX1, hZ1], 1);
|
|
5276
|
-
//draw sagittal
|
|
5277
|
-
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);
|
|
5278
6732
|
} else {
|
|
5279
|
-
|
|
5280
|
-
let
|
|
5281
|
-
let
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
]);
|
|
5297
|
-
|
|
5298
|
-
|
|
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;
|
|
5299
6798
|
}
|
|
5300
|
-
|
|
5301
|
-
if (this.isDragging && this.sliceType !== this.sliceTypeRender) {
|
|
6799
|
+
if (this.inRenderTile(this.dragStart[0], this.dragStart[1]) >= 0) return;
|
|
5302
6800
|
let width = Math.abs(this.dragStart[0] - this.dragEnd[0]);
|
|
5303
6801
|
let height = Math.abs(this.dragStart[1] - this.dragEnd[1]);
|
|
5304
|
-
|
|
5305
6802
|
this.drawSelectionBox([
|
|
5306
6803
|
Math.min(this.dragStart[0], this.dragEnd[0]),
|
|
5307
6804
|
Math.min(this.dragStart[1], this.dragEnd[1]),
|
|
@@ -5309,18 +6806,15 @@ Niivue.prototype.drawScene = function () {
|
|
|
5309
6806
|
height,
|
|
5310
6807
|
]);
|
|
5311
6808
|
}
|
|
5312
|
-
|
|
5313
6809
|
const pos = this.frac2mm([
|
|
5314
6810
|
this.scene.crosshairPos[0],
|
|
5315
6811
|
this.scene.crosshairPos[1],
|
|
5316
6812
|
this.scene.crosshairPos[2],
|
|
5317
6813
|
]);
|
|
5318
6814
|
this.drawGraph();
|
|
5319
|
-
|
|
5320
6815
|
posString =
|
|
5321
6816
|
pos[0].toFixed(2) + "×" + pos[1].toFixed(2) + "×" + pos[2].toFixed(2);
|
|
5322
6817
|
this.gl.finish();
|
|
5323
|
-
|
|
5324
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)
|
|
5325
6819
|
return posString;
|
|
5326
6820
|
}; // drawScene()
|