@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/src/niivue.js CHANGED
@@ -1,10 +1,16 @@
1
1
  import { Shader } from "./shader.js";
2
2
  import * as mat from "gl-matrix";
3
- import { vertSliceShader, fragSliceShader } from "./shader-srcs.js";
3
+ import {
4
+ vertSliceMMShader,
5
+ fragSliceMMShader,
6
+ vertSliceShader,
7
+ fragSliceShader,
8
+ } from "./shader-srcs.js";
4
9
  import {
5
10
  vertGraphShader,
11
+ vertRectShader,
6
12
  vertLineShader,
7
- fragLineShader,
13
+ fragRectShader,
8
14
  } from "./shader-srcs.js";
9
15
  import { vertRenderShader, fragRenderShader } from "./shader-srcs.js";
10
16
  import { vertColorbarShader, fragColorbarShader } from "./shader-srcs.js";
@@ -32,6 +38,7 @@ import {
32
38
  fragMeshOutlineShader,
33
39
  fragMeshHemiShader,
34
40
  fragMeshMatteShader,
41
+ fragMeshDepthShader,
35
42
  fragMeshShaderSHBlue,
36
43
  vertFiberShader,
37
44
  fragFiberShader,
@@ -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.drawUndoBitmap = null; //copy of prior drawBitmap
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.lineShader = null;
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.numScreenSlices = 0; // e.g. for multiplanar view, 3 simultaneous slices: axial, coronal, sagittal
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 (this.sliceType === this.sliceTypeRender) {
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.intensityRange$.next(this.volumes[volIdx]); //reference to volume to access cal_min and cal_max
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.intensityRange$.next(this.volumes[0]);
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.imageLoaded$.next(volume); // pass reference to the loaded NVImage (the volume)
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.imageLoaded$.next(mesh); // pass reference to the loaded NVImage (the volume)
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
- }; // mouseMove()
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
- }; // mouseMove()
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.sliceType != this.sliceTypeRender) return;
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.sliceType != this.sliceTypeRender) return;
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.drawUndoBitmap = null;
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
- for (let i = 1; i < nv; i++) if (img16[i] > 0) img16[i] = 10000;
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
- //for (let j = 0; j < 250; j++) {
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.COLOR_ATTACHMENT0;
2136
- let readTex = label1;
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.drawLine = function (ptA, ptB, penValue) {
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.drawUndoBitmap.length === this.drawBitmap.length
2877
+ this.drawUndoBitmaps[this.currentDrawUndoBitmap].length > 0
2421
2878
  ) {
2422
- let nv = this.drawUndoBitmap.length;
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 (this.drawUndoBitmap[i] === 0) continue;
2425
- this.drawBitmap[i] = this.drawUndoBitmap[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
- // line shader (crosshair)
2888
- this.lineShader = new Shader(this.gl, vertLineShader, fragLineShader);
2889
- this.graphShader = new Shader(this.gl, vertGraphShader, fragLineShader);
3345
+ // slice mm shader
3346
+ this.sliceMMShader = new Shader(
3347
+ this.gl,
3348
+ vertSliceMMShader,
3349
+ fragSliceMMShader
3350
+ );
3351
+ this.sliceMMShader.use(this.gl);
3352
+ this.gl.uniform1i(this.sliceMMShader.uniforms["volume"], 0);
3353
+ this.gl.uniform1i(this.sliceMMShader.uniforms["overlay"], 2);
3354
+ this.gl.uniform1i(this.sliceMMShader.uniforms["drawing"], 7);
3355
+ this.gl.uniform1f(
3356
+ this.sliceMMShader.uniforms["drawOpacity"],
3357
+ this.drawOpacity
3358
+ );
3359
+ // rect shader (crosshair): horizontal and vertical lines only
3360
+ this.rectShader = new Shader(this.gl, vertRectShader, fragRectShader);
3361
+ // line shader: diagonal lines
3362
+ this.lineShader = new Shader(this.gl, vertLineShader, fragRectShader);
3363
+ this.graphShader = new Shader(this.gl, vertGraphShader, fragRectShader);
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
- if (nLayer > 1) {
3602
- for (let i = 1; i < nLayer; i++) {
3603
- let lut = this.colormap(this.volumes[i].colorMap);
3604
- let c = new Uint8ClampedArray(luts.length + lut.length);
3605
- c.set(luts);
3606
- c.set(lut, luts.length);
3607
- luts = c;
3608
- } //colorMap
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
- var dims = [
3627
- 1.0,
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
- dims[1] / longestAxis,
3635
- dims[2] / longestAxis,
3636
- dims[3] / longestAxis,
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
- // not included in public docs
4229
+ function swizzleVec3(vec, order = [0, 1, 2]) {
4230
+ let vout = mat.vec3.create();
4231
+ vout[0] = vec[order[0]];
4232
+ vout[1] = vec[order[1]];
4233
+ vout[2] = vec[order[2]];
4234
+ return vout;
4235
+ }
4236
+
4237
+ Niivue.prototype.inRenderTile = function (x, y) {
4238
+ for (let i = 0; i < this.screenSlices.length; i++) {
4239
+ if (this.screenSlices[i].axCorSag !== this.sliceTypeRender) continue;
4240
+ let ltwh = this.screenSlices[i].leftTopWidthHeight;
4241
+ if (
4242
+ x > ltwh[0] &&
4243
+ y > ltwh[1] &&
4244
+ x < ltwh[0] + ltwh[2] &&
4245
+ y < ltwh[1] + ltwh[3]
4246
+ )
4247
+ return i;
4248
+ }
4249
+ return -1; //mouse position not in rendering tile
4250
+ };
4251
+
4252
+ Niivue.prototype.sliceScroll3D = function (posChange = 0) {
4253
+ if (posChange === 0) return;
4254
+ //n.b. clip plane only influences voxel-based volumes, so zoom is only action for meshes
4255
+ if (this.volumes.length > 0 && this.scene.clipPlaneDepthAziElev[0] < 1.8) {
4256
+ //clipping mode: change clip plane depth
4257
+ //if (this.scene.clipPlaneDepthAziElev[0] > 1.8) return;
4258
+ let depthAziElev = this.scene.clipPlaneDepthAziElev.slice();
4259
+ //bound clip sqrt(3) = 1.73
4260
+ if (posChange > 0) depthAziElev[0] = Math.min(1.5, depthAziElev[0] + 0.025);
4261
+ if (posChange < 0)
4262
+ depthAziElev[0] = Math.max(-1.5, depthAziElev[0] - 0.025); //Math.max(-1.7,
4263
+ if (depthAziElev[0] !== this.scene.clipPlaneDepthAziElev[0]) {
4264
+ this.scene.clipPlaneDepthAziElev = depthAziElev;
4265
+ return this.setClipPlane(this.scene.clipPlaneDepthAziElev);
4266
+ }
4267
+ return;
4268
+ }
4269
+ if (posChange > 0)
4270
+ this.volScaleMultiplier = Math.min(2.0, this.volScaleMultiplier * 1.1);
4271
+ if (posChange < 0)
4272
+ this.volScaleMultiplier = Math.max(0.5, this.volScaleMultiplier * 0.9);
4273
+ this.drawScene();
4274
+ };
4275
+
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
- if (posChange === 0) return;
3679
- //n.b. clip plane only influences voxel-based volumes, so zoom is only action for meshes
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.numScreenSlices < 1 ||
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.numScreenSlices; i++) {
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
- var ltwh = this.screenSlices[i].leftTopWidthHeight;
3719
- let isMirror = false;
3720
- if (ltwh[2] < 0) {
3721
- isMirror = true;
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.scene.location$.next({
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
- if (axCorSag === this.sliceTypeAxial) {
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.drawLine(pt, this.drawPenLocation, this.opts.penValue);
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.scene.location$.next({
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
- // not included in public docs
3814
- Niivue.prototype.drawSelectionBox = function (leftTopWidthHeight) {
4413
+ Niivue.prototype.drawRuler = function () {
4414
+ let fovMM = [];
4415
+ let ltwh = [];
4416
+ //leftTopWidthHeight
4417
+ for (let i = 0; i < this.screenSlices.length; i++) {
4418
+ if (this.screenSlices[i].axCorSag === this.sliceTypeRender) continue;
4419
+ //let ltwh = this.screenSlices[i].leftTopWidthHeight;
4420
+ if (this.screenSlices[i].fovMM.length > 1) {
4421
+ ltwh = this.screenSlices[i].leftTopWidthHeight;
4422
+ fovMM = this.screenSlices[i].fovMM;
4423
+ break;
4424
+ }
4425
+ }
4426
+ if (ltwh.length < 4) return;
4427
+ let frac10cm = 100.0 / fovMM[0];
4428
+ let pix10cm = frac10cm * ltwh[2];
4429
+ let pixLeft = ltwh[0] + 0.5 * ltwh[2] - 0.5 * pix10cm;
4430
+ let pixTop = ltwh[1] + ltwh[3] - 2 * this.opts.rulerWidth;
4431
+ let startXYendXY = [pixLeft, pixTop, pixLeft + pix10cm, pixTop];
4432
+ this.drawRuler10cm(startXYendXY);
4433
+ };
4434
+
4435
+ Niivue.prototype.drawRuler10cm = function (startXYendXY) {
4436
+ this.gl.bindVertexArray(this.genericVAO);
3815
4437
  this.lineShader.use(this.gl);
3816
4438
  this.gl.uniform4fv(
3817
4439
  this.lineShader.uniforms["lineColor"],
3818
- this.opts.selectionBoxColor
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
- this.gl.uniform4f(
3825
- this.lineShader.uniforms["leftTopWidthHeight"],
3826
- leftTopWidthHeight[0],
3827
- leftTopWidthHeight[1],
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.bindVertexArray(this.genericVAO);
4451
+ this.gl.uniform4fv(this.lineShader.uniforms["startXYendXY"], startXYendXY);
3832
4452
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
3833
- this.gl.bindVertexArray(this.unusedVAO); //switch off to avoid tampering with settings
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.drawColorbar = function (leftTopWidthHeight) {
3838
- if (leftTopWidthHeight[2] <= 0 || leftTopWidthHeight[3] <= 0) return;
3839
- // if (this.opts.crosshairWidth > 0) {
3840
- // //gl.disable(gl.DEPTH_TEST);
3841
- // this.lineShader.use(this.gl)
3842
- // this.gl.uniform4fv(this.lineShader.uniforms["lineColor"], this.opts.crosshairColor);
3843
- // this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [this.gl.canvas.width, this.gl.canvas.height]);
3844
- // let ltwh = [leftTopWidthHeight[0]-1, leftTopWidthHeight[1]-1, leftTopWidthHeight[2]+2, leftTopWidthHeight[3]+2];
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.colorbarShader.uniforms["leftTopWidthHeight"],
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
- this.gl.texParameteri(
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
- // not included in public docs
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
- }; // drawTextRight()
4816
+ }; // drawTextLeft()
4817
+
4818
+ // not included in public docs
4819
+ Niivue.prototype.drawTextRightBelow = function (
4820
+ xy,
4821
+ str,
4822
+ scale = 1,
4823
+ color = null
4824
+ ) {
4825
+ //to right of x, vertically centered on y
4826
+ if (this.opts.textHeight <= 0) return;
4827
+ let size = this.opts.textHeight * this.gl.canvas.height * scale;
4828
+ //xy[0] -= this.textWidth(size, str);
4829
+ //xy[1] -= 0.5 * size;
4830
+ this.drawText(xy, str, scale, color);
4831
+ }; // drawTextLeftBelow()
4832
+
4833
+ // not included in public docs
4834
+ Niivue.prototype.drawTextBetween = function (
4835
+ startXYendXY,
4836
+ str,
4837
+ scale = 1,
4838
+ color = null
4839
+ ) {
4840
+ //horizontally centered on x, below y
4841
+ if (this.opts.textHeight <= 0) return;
4842
+ let xy = [
4843
+ (startXYendXY[0] + startXYendXY[2]) * 0.5,
4844
+ (startXYendXY[1] + startXYendXY[3]) * 0.5,
4845
+ ];
4846
+ let size = this.opts.textHeight * this.gl.canvas.height * scale;
4847
+ let w = this.textWidth(size, str);
4848
+ xy[0] -= 0.5 * w;
4849
+ xy[1] -= 0.5 * size;
4850
+ let LTWH = [xy[0] - 1, xy[1] - 1, w + 2, size + 2];
4851
+ let clr = color;
4852
+ if (clr === null) clr = this.opts.crosshairColor;
4853
+ if (clr[0] + clr[1] + clr[2] > 0.8) clr = [0, 0, 0, 0.5];
4854
+ else clr = [1, 1, 1, 0.5];
4855
+ this.drawRect(LTWH, clr);
4856
+ this.drawText(xy, str, scale, color);
4857
+ }; // drawTextBetween()
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
- Niivue.prototype.calculateMvpMatrixX = function () {
4005
- let whratio = this.gl.canvas.clientWidth / this.gl.canvas.clientHeight;
4006
- //pivot from center of objects
4007
- //let scale = this.furthestVertexFromOrigin;
4008
- //let origin = [0,0,0];
4009
- let scale = this.furthestFromPivot;
4010
- let origin = [0, 0, 0];
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
- //scale = (0.8 * scale) / this.volScaleMultiplier; //2.0 WebGL viewport has range of 2.0 [-1,-1]...[1,1]
4013
- if (whratio < 1)
4014
- //tall window: "portrait" mode, width constrains
4015
- mat.mat4.ortho(
4016
- projectionMatrix,
4017
- -scale,
4018
- scale,
4019
- -scale / whratio,
4020
- scale / whratio,
4021
- 0.01,
4022
- scale * 8.0
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
- if (this.position)
4042
- mat.mat4.translate(modelMatrix, modelMatrix, this.position);
4043
-
4044
- mat.mat4.translate(modelMatrix, modelMatrix, [
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
- Niivue.prototype.drawMesh3DX = function (
4065
- isDepthTest = true,
4066
- alpha = 1.0,
4067
- leftTopWidthHeight
4954
+ return {
4955
+ modelViewProjectionMatrix,
4956
+ modelMatrix,
4957
+ normalMatrix,
4958
+ leftTopMM,
4959
+ fovMM,
4960
+ };
4961
+ }; // calculateMvpMatrix2D
4962
+
4963
+ Niivue.prototype.swizzleVec3MM = function (v3, axCorSag) {
4964
+ // change order of vector components
4965
+ if (axCorSag === this.sliceTypeCoronal) {
4966
+ //2D coronal screenXYZ = nifti [i,k,j]
4967
+ v3 = swizzleVec3(v3, [0, 2, 1]);
4968
+ } else if (axCorSag === this.sliceTypeSagittal) {
4969
+ //2D sagittal screenXYZ = nifti [j,k,i]
4970
+ v3 = swizzleVec3(v3, [1, 2, 0]);
4971
+ }
4972
+ return v3;
4973
+ }; // swizzleVec3MM()
4974
+
4975
+ Niivue.prototype.screenFieldOfViewMM = function (
4976
+ axCorSag = 0,
4977
+ forceSliceMM = false
4068
4978
  ) {
4069
- if (this.meshes.length < 1) return;
4070
- let gl = this.gl;
4071
- let m, modelMtx, normMtx;
4072
- [m, modelMtx, normMtx] = this.calculateMvpMatrixX(
4073
- this.volumeObject3D,
4074
- leftTopWidthHeight
4979
+ //extent of volume/mesh (in millimeters) in screen space
4980
+ if (!forceSliceMM && !this.opts.isSliceMM) {
4981
+ //return voxel space
4982
+ let fov = this.volumeObject3D.fieldOfViewDeObliqueMM.slice();
4983
+ return this.swizzleVec3MM(fov, axCorSag);
4984
+ }
4985
+ let mnMM = this.volumeObject3D.extentsMin.slice();
4986
+ let mxMM = this.volumeObject3D.extentsMax.slice();
4987
+ let rotation = mat.mat4.create(); //identity matrix: 2D axial screenXYZ = nifti [i,j,k]
4988
+ mnMM = this.swizzleVec3MM(mnMM, axCorSag);
4989
+ mxMM = this.swizzleVec3MM(mxMM, axCorSag);
4990
+ let fovMM = mat.vec3.create();
4991
+ mat.vec3.subtract(fovMM, mxMM, mnMM);
4992
+ return fovMM;
4993
+ };
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
- gl.enable(gl.DEPTH_TEST);
4078
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5033
+ this.drawTextRight(
5034
+ [
5035
+ leftTopWidthHeight[0],
5036
+ leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5,
5037
+ ],
5038
+ leftText
5039
+ );
5040
+ };
4079
5041
 
4080
- gl.disable(gl.BLEND);
4081
- gl.depthFunc(gl.GREATER);
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
- if (isDepthTest) {
4084
- gl.disable(gl.BLEND);
4085
- gl.depthFunc(gl.GREATER);
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
- gl.enable(gl.BLEND);
4088
- gl.depthFunc(gl.ALWAYS);
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
- gl.disable(gl.CULL_FACE); //show front and back faces
4091
- //Draw the mesh
4092
- this.meshShader.use(this.gl); // set Shader
4093
- //set shader uniforms
4094
- gl.uniformMatrix4fv(this.meshShader.uniforms["mvpMtx"], false, m);
4095
- gl.uniformMatrix4fv(this.meshShader.uniforms["modelMtx"], false, modelMtx);
4096
- gl.uniformMatrix4fv(this.meshShader.uniforms["normMtx"], false, normMtx);
4097
- gl.uniform1f(this.meshShader.uniforms["opacity"], alpha);
4098
- let hasFibers = false;
4099
-
4100
- for (let i = 0; i < this.meshes.length; i++) {
4101
- if (this.meshes[i].indexCount < 3) continue;
4102
- if (this.meshes[i].offsetPt0) {
4103
- hasFibers = true;
4104
- continue;
4105
- }
4106
- gl.bindVertexArray(this.meshes[i].vao);
4107
- gl.drawElements(
4108
- gl.TRIANGLES,
4109
- this.meshes[i].indexCount,
4110
- gl.UNSIGNED_INT,
4111
- 0
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
- let shader = this.fiberShader;
4122
- shader.use(this.gl);
4123
- gl.uniformMatrix4fv(shader.uniforms["mvpMtx"], false, m);
4124
- gl.uniform1f(shader.uniforms["opacity"], alpha);
4125
- for (let i = 0; i < this.meshes.length; i++) {
4126
- if (this.meshes[i].indexCount < 3) continue;
4127
- if (!this.meshes[i].offsetPt0) continue;
4128
- gl.bindVertexArray(this.meshes[i].vao);
4129
- gl.drawElements(
4130
- gl.LINE_STRIP,
4131
- this.meshes[i].indexCount,
4132
- gl.UNSIGNED_INT,
4133
- 0
5148
+ gl.enable(gl.DEPTH_TEST);
5149
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
5150
+ //draw the slice
5151
+ gl.disable(gl.BLEND);
5152
+ gl.depthFunc(gl.GREATER);
5153
+ gl.disable(gl.CULL_FACE); //show front and back faces
5154
+ this.sliceMMShader.use(this.gl);
5155
+ this.gl.uniform1i(
5156
+ this.sliceMMShader.uniforms["backgroundMasksOverlays"],
5157
+ this.backgroundMasksOverlays
5158
+ );
5159
+ this.gl.uniform1f(
5160
+ this.sliceMMShader.uniforms["opacity"],
5161
+ this.volumes[0].opacity
5162
+ );
5163
+ this.gl.uniform1i(this.sliceMMShader.uniforms["axCorSag"], axCorSag);
5164
+ this.gl.uniform1f(this.sliceMMShader.uniforms["slice"], sliceFrac);
5165
+ this.gl.uniformMatrix4fv(
5166
+ this.sliceMMShader.uniforms["frac2mm"],
5167
+ false,
5168
+ this.volumes[0].frac2mm
5169
+ );
5170
+ this.gl.uniformMatrix4fv(
5171
+ this.sliceMMShader.uniforms["mvpMtx"],
5172
+ false,
5173
+ obj.modelViewProjectionMatrix.slice()
5174
+ );
5175
+ this.gl.bindVertexArray(this.genericVAO); //set vertex attributes
5176
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
5177
+ this.gl.bindVertexArray(this.unusedVAO); //set vertex attributes
5178
+ //record screenSlices to detect mouse click positions
5179
+ this.screenSlices.push({
5180
+ leftTopWidthHeight: leftTopWidthHeight,
5181
+ axCorSag: axCorSag,
5182
+ sliceFrac: sliceFrac,
5183
+ AxyzMxy: this.xyMM2xyzMM(axCorSag, sliceFrac),
5184
+ leftTopMM: obj.leftTopMM,
5185
+ fovMM: obj.fovMM,
5186
+ });
5187
+ if (isNaN(customMM)) {
5188
+ //draw crosshairs
5189
+ this.drawCrosshairs3D(true, 1.0, obj.modelViewProjectionMatrix);
5190
+ }
5191
+ if (this.opts.meshThicknessOn2D > 0.0) {
5192
+ if (this.opts.meshThicknessOn2D !== Infinity)
5193
+ obj = this.calculateMvpMatrix2D(
5194
+ leftTopWidthHeight,
5195
+ screen.mnMM,
5196
+ screen.mxMM,
5197
+ this.opts.meshThicknessOn2D,
5198
+ sliceMM,
5199
+ azimuth,
5200
+ elevation,
5201
+ isRadiolgical
5202
+ );
5203
+ this.drawMesh3D(
5204
+ true,
5205
+ 1,
5206
+ obj.modelViewProjectionMatrix,
5207
+ obj.modelMatrix,
5208
+ obj.normalMatrix
4134
5209
  );
4135
- gl.bindVertexArray(this.unusedVAO);
4136
5210
  }
4137
- gl.enable(gl.BLEND);
4138
- gl.depthFunc(gl.ALWAYS);
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
- }; //drawMesh3D()
5218
+ }; // draw2DMM()
4141
5219
 
4142
5220
  // not included in public docs
4143
- Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
5221
+ Niivue.prototype.draw2DVox = function (
5222
+ leftTopWidthHeight,
5223
+ axCorSag,
5224
+ customMM = NaN
5225
+ ) {
5226
+ let fovMM = this.screenFieldOfViewMM(axCorSag);
5227
+ if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
5228
+ //use full screen
5229
+ leftTopWidthHeight = this.scaleSlice(fovMM[0], fovMM[1]);
5230
+ }
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
- var crossXYZ = [
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
- if (axCorSag === this.sliceTypeSagittal)
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[this.numScreenSlices].leftTopWidthHeight =
4201
- leftTopWidthHeight;
4202
- this.screenSlices[this.numScreenSlices].axCorSag = axCorSag;
4203
- this.numScreenSlices += 1;
5299
+ this.screenSlices.push({
5300
+ leftTopWidthHeight: leftTopWidthHeight,
5301
+ axCorSag: axCorSag,
5302
+ sliceFrac: crossXYZ[2],
5303
+ AxyzMxy: [],
5304
+ leftTopMM: [],
5305
+ fovMM: [fovMM[0], fovMM[1]],
5306
+ });
5307
+ gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5308
+ this.drawSliceOrientationText(ltwh, axCorSag);
5309
+ if (!isNaN(customMM)) return;
4204
5310
  if (this.opts.crosshairWidth <= 0.0) return;
4205
- this.lineShader.use(this.gl);
5311
+ this.gl.bindVertexArray(this.genericVAO);
5312
+ this.rectShader.use(this.gl);
4206
5313
  this.gl.uniform4fv(
4207
- this.lineShader.uniforms["lineColor"],
5314
+ this.rectShader.uniforms["lineColor"],
4208
5315
  this.opts.crosshairColor
4209
5316
  );
4210
- this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
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.lineShader.uniforms["leftTopWidthHeight"],
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.lineShader.uniforms["leftTopWidthHeight"],
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
- }; // draw2D()
5348
+ }; // draw2DVox()
4284
5349
 
4285
- Niivue.prototype.calculateMvpMatrix = function () {
4286
- function deg2rad(deg) {
4287
- return deg * (Math.PI / 180.0);
4288
- }
4289
- let whratio = this.gl.canvas.clientWidth / this.gl.canvas.clientHeight;
5350
+ Niivue.prototype.draw2D = function (
5351
+ leftTopWidthHeight,
5352
+ axCorSag,
5353
+ customMM = NaN
5354
+ ) {
5355
+ if (this.opts.isSliceMM)
5356
+ this.draw2DMM(leftTopWidthHeight, axCorSag, customMM);
5357
+ else this.draw2DVox(leftTopWidthHeight, axCorSag, customMM);
5358
+ };
5359
+
5360
+ Niivue.prototype.calculateMvpMatrix = function (
5361
+ unused,
5362
+ leftTopWidthHeight = [0, 0, 0, 0],
5363
+ azimuth,
5364
+ elevation
5365
+ ) {
5366
+ if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0)
5367
+ //use full canvas
5368
+ leftTopWidthHeight = [0, 0, this.gl.canvas.width, this.gl.canvas.height];
5369
+ let whratio = leftTopWidthHeight[2] / leftTopWidthHeight[3];
5370
+ //let whratio = this.gl.canvas.clientWidth / this.gl.canvas.clientHeight;
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.lineShader.use(this.gl);
4503
- this.gl.uniform4fv(this.lineShader.uniforms["lineColor"], color);
4504
- this.gl.uniform2fv(this.lineShader.uniforms["canvasWidthHeight"], [
5560
+ this.rectShader.use(this.gl);
5561
+ this.gl.uniform4fv(this.rectShader.uniforms["lineColor"], color);
5562
+ this.gl.uniform2fv(this.rectShader.uniforms["canvasWidthHeight"], [
4505
5563
  this.gl.canvas.width,
4506
5564
  this.gl.canvas.height,
4507
5565
  ]);
4508
5566
  this.gl.uniform4f(
4509
- this.lineShader.uniforms["leftTopWidthHeight"],
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.numScreenSlices; i++) {
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(5, mn, mx);
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
- gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
5843
+ if (mvpMatrix === null)
5844
+ [mvpMatrix, modelMatrix, normalMatrix] = this.calculateMvpMatrix(
5845
+ null,
5846
+ leftTopWidthHeight,
5847
+ azimuth,
5848
+ elevation
5849
+ );
5850
+ if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
5851
+ //use full canvas
5852
+ leftTopWidthHeight = [0, 0, gl.canvas.width, gl.canvas.height];
5853
+ this.screenSlices.push({
5854
+ leftTopWidthHeight: leftTopWidthHeight,
5855
+ axCorSag: this.sliceTypeRender,
5856
+ sliceFrac: 0,
5857
+ AxyzMxy: [],
5858
+ leftTopMM: [],
5859
+ fovMM: [isRadiological(modelMatrix), 0],
5860
+ });
5861
+ } else {
5862
+ this.screenSlices.push({
5863
+ leftTopWidthHeight: leftTopWidthHeight.slice(),
5864
+ axCorSag: this.sliceTypeRender,
5865
+ sliceFrac: 0,
5866
+ AxyzMxy: [],
5867
+ leftTopMM: [],
5868
+ fovMM: [isRadiological(modelMatrix), 0],
5869
+ });
5870
+ leftTopWidthHeight[1] =
5871
+ gl.canvas.height - leftTopWidthHeight[3] - leftTopWidthHeight[1];
5872
+ }
5873
+ gl.viewport(
5874
+ leftTopWidthHeight[0],
5875
+ leftTopWidthHeight[1],
5876
+ leftTopWidthHeight[2],
5877
+ leftTopWidthHeight[3]
5878
+ );
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
- let mvpMatrix, modelMatrix, normalMatrix;
4793
- // eslint-disable-next-line no-unused-vars
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 = (this.mousePos[0] * gl.canvas.width) / gl.canvas.clientWidth;
5912
+ const pixelX =
5913
+ (this.mousePos[0] * leftTopWidthHeight[2]) / leftTopWidthHeight[2];
4819
5914
  const pixelY =
4820
5915
  gl.canvas.height -
4821
- (this.mousePos[1] * gl.canvas.height) / gl.canvas.clientHeight -
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 (isDepthTest = true, alpha = 1.0) {
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
- [m, modelMtx, normMtx] = this.calculateMvpMatrix(this.volumeObject3D);
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 (isDepthTest = true, alpha = 1.0) {
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 = Math.min(
4968
- Math.min(this.back.pixDims[1], this.back.pixDims[2]),
4969
- this.back.pixDims[3]
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 m, modelMtx, normMtx;
6098
+ let modelMtx, normMtx;
4987
6099
  // eslint-disable-next-line no-unused-vars
4988
- [m, modelMtx, normMtx] = this.calculateMvpMatrix(this.crosshairs3D);
4989
- gl.uniformMatrix4fv(crosshairsShader.uniforms["mvpMtx"], false, m);
6100
+ if (mvpMtx == null)
6101
+ [mvpMtx, modelMtx, normMtx] = this.calculateMvpMatrix(
6102
+ this.crosshairs3D,
6103
+ this.scene.renderAzimuth,
6104
+ this.scene.renderElevation
6105
+ );
6106
+ gl.uniformMatrix4fv(crosshairsShader.uniforms["mvpMtx"], false, mvpMtx);
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
- //let d = overlayItem.hdr.dims;
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
- }; // frac2mm()
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
- var ltwh = this.screenSlices[i].leftTopWidthHeight;
5101
- let isMirror = false;
5102
- if (ltwh[2] < 0) {
5103
- isMirror = true;
5104
- ltwh[0] += ltwh[2];
5105
- ltwh[2] = -ltwh[2];
6194
+ Niivue.prototype.screenXY2TextureFrac = function (x, y, i) {
6195
+ let texFrac = [-1, -1, -1]; //texture 0..1 so -1 is out of bounds
6196
+ var axCorSag = this.screenSlices[i].axCorSag;
6197
+ if (axCorSag > this.sliceTypeSagittal) return texFrac;
6198
+ var ltwh = this.screenSlices[i].leftTopWidthHeight.slice();
6199
+ let isMirror = false;
6200
+ if (ltwh[2] < 0) {
6201
+ isMirror = true;
6202
+ ltwh[0] += ltwh[2];
6203
+ ltwh[2] = -ltwh[2];
6204
+ }
6205
+ var fracX = (x - ltwh[0]) / ltwh[2];
6206
+ if (isMirror) fracX = 1.0 - fracX;
6207
+ var fracY = 1.0 - (y - ltwh[1]) / ltwh[3];
6208
+ if (fracX < 0.0 || fracX > 1.0 || fracY < 0.0 || fracY > 1.0) return texFrac;
6209
+ if (this.screenSlices[i].AxyzMxy.length > 4) {
6210
+ let xyzMM = [0, 0, 0];
6211
+ xyzMM[0] =
6212
+ this.screenSlices[i].leftTopMM[0] + fracX * this.screenSlices[i].fovMM[0];
6213
+ xyzMM[1] =
6214
+ this.screenSlices[i].leftTopMM[1] + fracY * this.screenSlices[i].fovMM[1];
6215
+ // let xyz = mat.vec3.fromValues(30, 30, 0);
6216
+ let v = this.screenSlices[i].AxyzMxy;
6217
+ xyzMM[2] = v[2] + v[4] * (xyzMM[1] - v[1]) - v[3] * (xyzMM[0] - v[0]);
6218
+ if (axCorSag === this.sliceTypeCoronal)
6219
+ xyzMM = swizzleVec3(xyzMM, [0, 2, 1]); //screen RSA to NIfTI RAS
6220
+ if (axCorSag === this.sliceTypeSagittal)
6221
+ xyzMM = swizzleVec3(xyzMM, [2, 0, 1]); //screen ASR to NIfTI RAS
6222
+ let xyz = this.mm2frac(xyzMM);
6223
+ if (
6224
+ xyz[0] < 0 ||
6225
+ xyz[0] > 1 ||
6226
+ xyz[1] < 0 ||
6227
+ xyz[1] > 1 ||
6228
+ xyz[2] < 0 ||
6229
+ xyz[2] > 1
6230
+ )
6231
+ return texFrac;
6232
+ texFrac[0] = xyz[0];
6233
+ texFrac[1] = xyz[1];
6234
+ texFrac[2] = xyz[2];
6235
+ } else {
6236
+ texFrac = this.scene.crosshairPos.slice();
6237
+ if (axCorSag === this.sliceTypeAxial) {
6238
+ texFrac[0] = fracX;
6239
+ texFrac[1] = fracY;
5106
6240
  }
5107
-
5108
- var fracX = (canvasPos[0] - ltwh[0]) / ltwh[2];
5109
- if (isMirror) {
5110
- fracX = 1.0 - fracX;
6241
+ if (axCorSag === this.sliceTypeCoronal) {
6242
+ texFrac[0] = fracX;
6243
+ texFrac[2] = fracY;
5111
6244
  }
5112
-
5113
- var fracY = 1.0 - (canvasPos[1] - ltwh[1]) / ltwh[3];
5114
- if (fracX >= 0.0 && fracX < 1.0 && fracY >= 0.0 && fracY < 1.0) {
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
- return [x, y, z];
5145
- }; // canvas2frac
6253
+ // not included in public docs
6254
+ Niivue.prototype.canvasPos2frac = function (canvasPos) {
6255
+ for (let i = 0; i < this.screenSlices.length; i++) {
6256
+ let texFrac = this.screenXY2TextureFrac(canvasPos[0], canvasPos[1], i);
6257
+ if (texFrac[0] >= 0) return texFrac;
6258
+ }
6259
+ return [-1, -1, -1]; //texture 0..1 so -1 is out of bounds
6260
+ };
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.gl.canvas.clientHeight)
5152
- scalePix = this.gl.canvas.clientHeight / h;
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.gl.canvas.clientHeight - hPix) * 0.5,
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
- if (this.sliceType === this.sliceTypeRender) {
5217
- //draw rendering
5218
- if (this.isDragging && this.scene.clipPlaneDepthAziElev[0] < 1.8) {
5219
- //if (this.scene.clipPlaneDepthAziElev[0] > 1.8) return;
5220
- let x = this.dragStart[0] - this.dragEnd[0];
5221
- let y = this.dragStart[1] - this.dragEnd[1];
5222
- let depthAziElev = this.dragClipPlaneStartDepthAziElev.slice();
5223
- depthAziElev[1] -= x;
5224
- depthAziElev[1] = depthAziElev[1] % 360;
5225
- depthAziElev[2] += y;
5226
- //gimbal lock: these next two lines could be changed - when we go over the pole, the Azimuth reverses
5227
- //if (depthAziElev[2] > 90) depthAziElev[2] = 90;
5228
- //if (depthAziElev[2] < -90) depthAziElev[2] = -90;
5229
- if (
5230
- depthAziElev[1] !== this.scene.clipPlaneDepthAziElev[1] ||
5231
- depthAziElev[2] !== this.scene.clipPlaneDepthAziElev[2]
5232
- ) {
5233
- this.scene.clipPlaneDepthAziElev = depthAziElev;
5234
- return this.setClipPlane(this.scene.clipPlaneDepthAziElev);
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
- return this.draw3D();
6709
+ } //dragging over rendering
6710
+ if (
6711
+ this.sliceMosaicString.length < 1 &&
6712
+ this.sliceType === this.sliceTypeRender
6713
+ ) {
6714
+ this.screenSlices = []; // empty array
6715
+ this.draw3D();
6716
+ if (this.opts.isColorbar) this.drawColorbar();
6717
+ return;
5238
6718
  }
5239
- let { volScale } = this.sliceScale();
5240
- this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height);
5241
- this.numScreenSlices = 0;
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
- //sliceTypeMultiplanar
5256
- let ltwh = this.scaleSlice(
5257
- volScale[0] + volScale[1],
5258
- volScale[1] + volScale[2]
5259
- );
5260
- let wX = (ltwh[2] * volScale[0]) / (volScale[0] + volScale[1]);
5261
- let ltwh3x1 = this.scaleSlice(
5262
- volScale[0] + volScale[0] + volScale[1],
5263
- Math.max(volScale[1], volScale[2])
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
- let wY = ltwh[2] - wX;
5280
- let hY = (ltwh[3] * volScale[1]) / (volScale[1] + volScale[2]);
5281
- let hZ = ltwh[3] - hY;
5282
- //draw axial
5283
- this.draw2D([ltwh[0], ltwh[1] + hZ, wX, hY], 0);
5284
- //draw coronal
5285
- this.draw2D([ltwh[0], ltwh[1], wX, hZ], 1);
5286
- //draw sagittal
5287
- this.draw2D([ltwh[0] + wX, ltwh[1], wY, hZ], 2);
5288
- //draw colorbar (optional) // TODO currently only drawing one colorbar, there may be one per overlay + one for the background
5289
- var margin = this.opts.colorBarMargin * hY;
5290
- if (!this.graph.autoSizeMultiplanar) {
5291
- this.drawColorbar([
5292
- ltwh[0] + wX + margin,
5293
- ltwh[1] + hZ + margin,
5294
- wY - margin - margin,
5295
- hY * this.opts.colorbarHeight,
5296
- ]);
5297
- }
5298
- // drawTextBelow(gl, [ltwh[0]+ wX + (wY * 0.5), ltwh[1] + hZ + margin + hY * colorbarHeight], "Syzygy"); //DEMO
6733
+ //sliceTypeMultiplanar
6734
+ let { volScale, vox, longestAxis } = this.sliceScale();
6735
+ let ltwh = this.scaleSlice(
6736
+ volScale[0] + volScale[1],
6737
+ volScale[1] + volScale[2]
6738
+ );
6739
+ let wX = (ltwh[2] * volScale[0]) / (volScale[0] + volScale[1]);
6740
+ let ltwh3x1 = this.scaleSlice(
6741
+ volScale[0] + volScale[0] + volScale[1],
6742
+ Math.max(volScale[1], volScale[2])
6743
+ );
6744
+ let mx = Math.max(Math.max(volScale[1], volScale[2]), volScale[0]);
6745
+ let ltwh4x1 = this.scaleSlice(
6746
+ volScale[0] + volScale[0] + volScale[1] + mx,
6747
+ mx
6748
+ );
6749
+ let wX1 =
6750
+ (ltwh3x1[2] * volScale[0]) / (volScale[0] + volScale[0] + volScale[1]);
6751
+ if (wX1 > wX && !this.graph.autoSizeMultiplanar) {
6752
+ //landscape screen ratio: 3 slices in single row
6753
+ let pixScale = wX1 / volScale[0];
6754
+ let hY1 = volScale[1] * pixScale;
6755
+ let hZ1 = volScale[2] * pixScale;
6756
+ if (ltwh3x1[3] === ltwh4x1[3]) {
6757
+ ltwh3x1 = ltwh4x1;
6758
+ this.draw3D([
6759
+ ltwh3x1[0] + wX1 + wX1 + hY1,
6760
+ ltwh3x1[1],
6761
+ ltwh4x1[3],
6762
+ ltwh4x1[3],
6763
+ ]);
6764
+ }
6765
+ //draw axial
6766
+ this.draw2D([ltwh3x1[0], ltwh3x1[1], wX1, hY1], 0);
6767
+ //draw coronal
6768
+ this.draw2D([ltwh3x1[0] + wX1, ltwh3x1[1], wX1, hZ1], 1);
6769
+ //draw sagittal
6770
+ this.draw2D([ltwh3x1[0] + wX1 + wX1, ltwh3x1[1], hY1, hZ1], 2);
6771
+ } else {
6772
+ let wY = ltwh[2] - wX;
6773
+ let hY = (ltwh[3] * volScale[1]) / (volScale[1] + volScale[2]);
6774
+ let hZ = ltwh[3] - hY;
6775
+ //draw axial
6776
+ this.draw2D([ltwh[0], ltwh[1] + hZ, wX, hY], 0);
6777
+ //draw coronal
6778
+ this.draw2D([ltwh[0], ltwh[1], wX, hZ], 1);
6779
+ //draw sagittal
6780
+ this.draw2D([ltwh[0] + wX, ltwh[1], wY, hZ], 2);
6781
+ if (!this.graph.autoSizeMultiplanar)
6782
+ this.draw3D([ltwh[0] + wX, ltwh[1] + hZ, wY, hY]);
6783
+ } //if landscape else portrait
6784
+ } //if multiplanar
6785
+ } //if mosaic not 2D
6786
+ if (this.opts.isRuler) this.drawRuler();
6787
+ if (this.opts.isColorbar) this.drawColorbar();
6788
+
6789
+ if (this.isDragging) {
6790
+ if (this.opts.isDragShowsMeasurementTool) {
6791
+ this.drawMeasurementTool([
6792
+ this.dragStart[0],
6793
+ this.dragStart[1],
6794
+ this.dragEnd[0],
6795
+ this.dragEnd[1],
6796
+ ]);
6797
+ return;
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()