@niivue/niivue 0.24.2 → 0.24.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
@@ -51,6 +51,9 @@ import { Log } from "./logger";
51
51
  import defaultFontPNG from "./fonts/Roboto-Regular.png";
52
52
  import defaultFontMetrics from "./fonts/Roboto-Regular.json";
53
53
  import { colortables } from "./colortables";
54
+ import { webSocket } from "rxjs/webSocket";
55
+ import { interval } from "rxjs";
56
+
54
57
  const log = new Log();
55
58
  const cmapper = new colortables();
56
59
 
@@ -119,7 +122,9 @@ export function Niivue(options = {}) {
119
122
  this.volumeTexture = null;
120
123
  this.drawTexture = null; //the GPU memory storage of the drawing
121
124
  this.drawBitmap = null; //the CPU memory storage of the drawing
122
- this.drawUndoBitmap = null; //copy of prior drawBitmap
125
+ this.maxDrawUndoBitmaps = 8; //analogy: number of bullets in revolver, we will cycle these
126
+ this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //analogy: cylinder position of a revolver
127
+ this.drawUndoBitmaps = [];
123
128
  this.drawOpacity = 0.8;
124
129
  this.drawPenLocation = [NaN, NaN, NaN];
125
130
  this.drawPenAxCorSag = -1; //do not allow pen to drag between Sagittal/Coronal/Axial
@@ -257,6 +262,14 @@ export function Niivue(options = {}) {
257
262
  },
258
263
  ];
259
264
 
265
+ // multiuser
266
+ this.isController = false;
267
+ this.isInSession = false;
268
+ this.sessionKey = "";
269
+ this.sessionUrl = "";
270
+ this.serverConnection$ = null;
271
+ this.interval$ = null;
272
+
260
273
  this.initialized = false;
261
274
  // loop through known Niivue properties
262
275
  // if the user supplied opts object has a
@@ -434,6 +447,142 @@ Niivue.prototype.sync = function () {
434
447
  this.otherNV.drawScene();
435
448
  };
436
449
 
450
+ // Internal function to connect to web socket server
451
+ Niivue.prototype.connectToServer = function (wsServerUrl, sessionName) {
452
+ const url = new URL(wsServerUrl);
453
+ url.pathname = "websockets";
454
+ url.search = "?session=" + sessionName;
455
+ this.serverConnection$ = webSocket(url.href);
456
+ console.log(url.href);
457
+ };
458
+
459
+ // Internal function to schedule updates to the server
460
+ Niivue.prototype.setUpdateInterval = function () {
461
+ this.interval$ = interval(300);
462
+ this.interval$.subscribe(() => {
463
+ this.serverConnection$.next({
464
+ op: "update",
465
+ azimuth: this.scene.renderAzimuth,
466
+ elevation: this.scene.renderElevation,
467
+ clipPlane: this.scene.clipPlane,
468
+ zoom: this.volScaleMultiplier,
469
+ key: this.sessionKey,
470
+ });
471
+ });
472
+ };
473
+
474
+ // Internal function called after a connection with the server has been made
475
+ Niivue.prototype.subscribeToServer = function (
476
+ sessionCreatedCallback,
477
+ sessionJoinedCallback
478
+ ) {
479
+ this.serverConnection$.subscribe({
480
+ next: (msg) => {
481
+ switch (msg["op"]) {
482
+ case "update":
483
+ this.scene.renderAzimuth = msg["azimuth"];
484
+ this.scene.renderElevation = msg["elevation"];
485
+ this.volScaleMultiplier = msg["zoom"];
486
+ this.scene.clipPlane = msg["clipPlane"];
487
+ this.drawScene();
488
+ break;
489
+
490
+ case "create":
491
+ console.log(msg);
492
+ if (!msg["isError"]) {
493
+ this.isInSession = true;
494
+ this.sessionKey = msg["key"];
495
+ this.setUpdateInterval();
496
+ }
497
+ if (sessionCreatedCallback) {
498
+ sessionCreatedCallback(
499
+ msg["message"],
500
+ msg["url"],
501
+ msg["key"],
502
+ msg["isError"]
503
+ );
504
+ }
505
+ break;
506
+
507
+ case "join":
508
+ this.isInSession = true;
509
+ this.isController = msg["isController"];
510
+ if (this.isController) {
511
+ this.setUpdateInterval();
512
+ }
513
+
514
+ if (sessionJoinedCallback) {
515
+ sessionJoinedCallback(
516
+ msg["message"],
517
+ msg["url"],
518
+ msg["isController"]
519
+ );
520
+ }
521
+ break;
522
+ }
523
+ }, // Called whenever there is a message from the server.
524
+ error: (err) => console.log(err), // Called if at any point WebSocket API signals some kind of error.
525
+ complete: () => console.log("complete"), // Called when connection is closed (for whatever reason).
526
+ });
527
+ };
528
+
529
+ /**
530
+ * Create a multiuser session
531
+ * @param {string} wsServerUrl e.g. ws://localhost:3000
532
+ * @param {string} sessionName
533
+ * @param {function(string, string, string, boolean):void} sessionCreatedCallback callback after session has been created with message, session url, session key
534
+ * if there was no error.
535
+ */
536
+ Niivue.prototype.createSession = function (
537
+ wsServerUrl,
538
+ sessionName,
539
+ sessionCreatedCallback
540
+ ) {
541
+ this.connectToServer(wsServerUrl, sessionName);
542
+
543
+ // subscribe to any messages from the server
544
+ this.subscribeToServer(sessionCreatedCallback);
545
+
546
+ // tell the server we want to create a sesion
547
+ this.serverConnection$.next({
548
+ op: "create",
549
+ });
550
+ };
551
+
552
+ /**
553
+ * Join a multiuser session
554
+ * @param {string} wsServerUrl e.g. ws://localhost:3000
555
+ * @param {string} sessionName
556
+ * @param {function(string, string, boolean):void} sessionJoinedCallback callback after session has been joined with message, session url and session key
557
+ */
558
+ Niivue.prototype.joinSession = function (
559
+ wsServerUrl,
560
+ sessionName,
561
+ key,
562
+ sessionJoinedCallback
563
+ ) {
564
+ this.connectToServer(wsServerUrl, sessionName);
565
+
566
+ // subscribe to any messages from the server
567
+ this.subscribeToServer(null, sessionJoinedCallback);
568
+
569
+ // tell the server we want to create a sesion
570
+ this.serverConnection$.next({
571
+ op: "join",
572
+ key: key,
573
+ });
574
+ };
575
+
576
+ /**
577
+ * Close a multiuser session
578
+ */
579
+ Niivue.prototype.closeSession = function () {
580
+ this.interval$.complete();
581
+ this.serverConnection$.complete();
582
+ this.isInSession = false;
583
+ this.isController = false;
584
+ };
585
+
437
586
  /* Not documented publicly for now
438
587
  * test if two arrays have equal values for each element
439
588
  * @param {Array} a the first array
@@ -635,6 +784,7 @@ Niivue.prototype.mouseUpListener = function () {
635
784
  this.scene.mouseButtonRightDown = false;
636
785
  this.scene.mouseButtonLeftDown = false;
637
786
  if (this.drawPenFillPts.length > 0) this.drawPenFilled();
787
+ else if (this.drawPenAxCorSag >= 0) this.drawAddUndoBitmap();
638
788
  this.drawPenLocation = [NaN, NaN, NaN];
639
789
  this.drawPenAxCorSag = -1;
640
790
  if (this.isDragging) {
@@ -1196,8 +1346,176 @@ Niivue.prototype.getVolumeIndexByID = function (id) {
1196
1346
  return -1; // -1 signals that no valid index was found for a volume with the given id
1197
1347
  };
1198
1348
 
1349
+ //inputs
1350
+ // data: Uint8Array to compress
1351
+ //output
1352
+ // returns rle compressed Uint8Array
1353
+
1354
+ function encodeRLE(data) {
1355
+ //https://en.wikipedia.org/wiki/PackBits
1356
+ //run length encoding
1357
+ // input and output are Uint8Array
1358
+ // Will compress data with long runs up to x64
1359
+ // Worst case encoded size is ~1% larger than input
1360
+ let dl = data.length; //input length
1361
+ let dp = 0; //input position
1362
+ //worst case: run length encoding (1+1/127) times larger than input
1363
+ let r = new Uint8Array(dl + Math.ceil(0.01 * dl));
1364
+ let rI = new Int8Array(r.buffer); //typecast as header can be negative
1365
+ let rp = 0; //run length position
1366
+ while (dp < dl) {
1367
+ //for each byte in input
1368
+ let v = data[dp];
1369
+ dp++;
1370
+ let rl = 1; //run length
1371
+ while (rl < 129 && dp < dl && data[dp] === v) {
1372
+ dp++;
1373
+ rl++;
1374
+ } //count run length
1375
+ if (rl > 1) {
1376
+ //header
1377
+ rI[rp] = -rl + 1;
1378
+ rp++;
1379
+ r[rp] = v;
1380
+ rp++;
1381
+ continue;
1382
+ }
1383
+ //count literal length
1384
+ while (dp < dl) {
1385
+ if (rl > 127) break;
1386
+ if (dp + 2 < dl) {
1387
+ //console.log(':', v, data[dp], data[dp+1]);
1388
+ if (
1389
+ v !== data[dp] &&
1390
+ data[dp + 2] === data[dp] &&
1391
+ data[dp + 1] === data[dp]
1392
+ )
1393
+ break;
1394
+ }
1395
+ v = data[dp];
1396
+ dp++;
1397
+ rl++;
1398
+ }
1399
+ //write header
1400
+ r[rp] = rl - 1;
1401
+ rp++;
1402
+ for (let i = 0; i < rl; i++) {
1403
+ r[rp] = data[dp - rl + i];
1404
+ rp++;
1405
+ }
1406
+ }
1407
+ log.info("PackBits " + dl + " -> " + rp + " bytes (x" + dl / rp + ")");
1408
+ return r.slice(0, rp);
1409
+ } // encodeRLE()
1410
+
1411
+ //inputs
1412
+ // rle: packbits compressed stream
1413
+ // decodedlen: size of uncompressed data
1414
+ //output
1415
+ // returns Uint8Array of decodedlen bytes
1416
+ function decodeRLE(rle, decodedlen) {
1417
+ let r = new Uint8Array(rle.buffer);
1418
+ let rI = new Int8Array(r.buffer); //typecast as header can be negative
1419
+ let rp = 0; //input position in rle array
1420
+ //d: output uncompressed data array
1421
+ let d = new Uint8Array(decodedlen);
1422
+ let dp = 0; //output position in decoded array
1423
+ while (rp < r.length) {
1424
+ //read header
1425
+ let hdr = rI[rp];
1426
+ rp++;
1427
+ if (hdr < 0) {
1428
+ //write run
1429
+ let v = rI[rp];
1430
+ rp++;
1431
+ for (let i = 0; i < 1 - hdr; i++) {
1432
+ d[dp] = v;
1433
+ dp++;
1434
+ }
1435
+ } else {
1436
+ //write literal
1437
+ for (let i = 0; i < hdr + 1; i++) {
1438
+ d[dp] = rI[rp];
1439
+ rp++;
1440
+ dp++;
1441
+ }
1442
+ } //if run else literal
1443
+ } //while rp < r.length
1444
+ return d;
1445
+ } // decodeRLE()
1446
+
1447
+ /*Niivue.prototype.testRLE = function() {
1448
+ //Demo to test encodeRLE/decodeRLE
1449
+ let len = 256*256*256;
1450
+ let data = new Uint8Array(len);
1451
+ for (let i = 0; i < len; i++)
1452
+ data[i] = ((Math.random() > 0.30) ? 1 : 0);
1453
+ console.log(data);
1454
+ let rle = encodeRLE(data);
1455
+ let ddata = decodeRLE(rle, len);
1456
+ let ok = true;
1457
+ for (let i = 0; i < len; i++)
1458
+ if (data[i] !== ddata[i])
1459
+ ok = false;
1460
+ console.log('decoded correctly:', ok);
1461
+ } // testRLE()*/
1462
+
1463
+ Niivue.prototype.drawAddUndoBitmap = async function (fnm) {
1464
+ if (!this.drawBitmap || this.drawBitmap.length < 1) {
1465
+ console.log("drawAddUndoBitmap error: No drawing open");
1466
+ return false;
1467
+ }
1468
+ //let rle = encodeRLE(this.drawBitmap);
1469
+ //the bitmaps are a cyclical loop, like a revolver hand gun: increment the cylinder
1470
+ this.currentDrawUndoBitmap++;
1471
+ if (this.currentDrawUndoBitmap >= this.maxDrawUndoBitmaps)
1472
+ this.currentDrawUndoBitmap = 0;
1473
+ this.drawUndoBitmaps[this.currentDrawUndoBitmap] = encodeRLE(this.drawBitmap);
1474
+ }; // drawAddUndoBitmap()
1475
+
1476
+ Niivue.prototype.drawClearAllUndoBitmaps = async function () {
1477
+ this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //next add will be cylinder 0
1478
+ if (this.drawUndoBitmaps.length < 1) return;
1479
+ for (let i = this.drawUndoBitmaps.length - 1; i >= 0; i--) array[i] = [];
1480
+ }; // drawClearAllUndoBitmaps()
1481
+
1482
+ Niivue.prototype.drawUndo = function () {
1483
+ if (this.drawUndoBitmaps.length < 1) {
1484
+ console.log("undo bitmaps not loaded");
1485
+ return;
1486
+ }
1487
+ //let dims = this.back.hdr.dims;
1488
+ //let nv = dims[1] * dims[2] * dims[3];
1489
+ this.currentDrawUndoBitmap--;
1490
+ if (this.currentDrawUndoBitmap < 0)
1491
+ this.currentDrawUndoBitmap = this.drawUndoBitmaps.length - 1;
1492
+ if (this.currentDrawUndoBitmap >= this.drawUndoBitmaps.length)
1493
+ this.currentDrawUndoBitmap = 0;
1494
+ if (this.drawUndoBitmaps[this.currentDrawUndoBitmap].length < 2) {
1495
+ console.log("drawUndo is misbehaving");
1496
+ return;
1497
+ }
1498
+ this.drawBitmap = decodeRLE(
1499
+ this.drawUndoBitmaps[this.currentDrawUndoBitmap],
1500
+ this.drawBitmap.length
1501
+ );
1502
+ this.refreshDrawing(true);
1503
+ };
1504
+
1505
+ /*Niivue.prototype.drawUndo = function () {
1506
+ let hdr = this.back.hdr;
1507
+ let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
1508
+ if (this.drawUndoBitmap.length !== nv || this.drawBitmap.length !== nv) {
1509
+ console.log("bitmap dims are wrong");
1510
+ return;
1511
+ }
1512
+ this.drawBitmap = this.drawUndoBitmap.slice();
1513
+ this.refreshDrawing(true);
1514
+ };*/
1515
+
1199
1516
  Niivue.prototype.loadDrawing = async function (fnm) {
1200
1517
  if (this.drawBitmap) console.log("Overwriting open drawing!");
1518
+ this.drawClearAllUndoBitmaps();
1201
1519
  let volume = await NVImage.loadFromUrl({ url: fnm });
1202
1520
  //let volume = await NVImage.loadFromFile({file: fnm});
1203
1521
  let dims = volume.hdr.dims; //reverse to original
@@ -1215,7 +1533,7 @@ Niivue.prototype.loadDrawing = async function (fnm) {
1215
1533
  //if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {
1216
1534
  let vx = dims[1] * dims[2] * dims[3];
1217
1535
  //this.drawBitmap = new Uint8Array(vx);
1218
- this.drawUndoBitmap = null;
1536
+ //this.drawUndoBitmap = null;
1219
1537
  this.drawBitmap = new Uint8Array(vx);
1220
1538
  this.drawTexture = this.r8Tex(
1221
1539
  this.drawTexture,
@@ -1273,6 +1591,7 @@ Niivue.prototype.loadDrawing = async function (fnm) {
1273
1591
  } //for x
1274
1592
  } //for y
1275
1593
  } //for z
1594
+ this.drawAddUndoBitmap();
1276
1595
  this.refreshDrawing(false);
1277
1596
  this.drawScene();
1278
1597
  };
@@ -1715,6 +2034,11 @@ Niivue.prototype.setScale = function (scale) {
1715
2034
  */
1716
2035
  Niivue.prototype.setClipPlaneColor = function (color) {
1717
2036
  this.opts.clipPlaneColor = color;
2037
+ this.renderShader.use(this.gl);
2038
+ this.gl.uniform4fv(
2039
+ this.renderShader.uniforms["clipPlaneColor"],
2040
+ this.opts.clipPlaneColor
2041
+ );
1718
2042
  this.drawScene();
1719
2043
  }; // setClipPlaneColor()
1720
2044
 
@@ -1915,7 +2239,9 @@ Niivue.prototype.createEmptyDrawing = function () {
1915
2239
  if (mn < 1) return; //something is horribly wrong!
1916
2240
  let vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
1917
2241
  this.drawBitmap = new Uint8Array(vx);
1918
- this.drawUndoBitmap = null;
2242
+ this.drawClearAllUndoBitmaps();
2243
+ this.drawAddUndoBitmap();
2244
+ //this.drawUndoBitmap = null;
1919
2245
  this.drawTexture = this.r8Tex(
1920
2246
  this.drawTexture,
1921
2247
  this.gl.TEXTURE7,
@@ -2038,16 +2364,6 @@ function img2ras16(volume) {
2038
2364
  return img16;
2039
2365
  }
2040
2366
 
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
2367
  Niivue.prototype.drawGrowCut = function () {
2052
2368
  let hdr = this.back.hdr;
2053
2369
  let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
@@ -2055,7 +2371,7 @@ Niivue.prototype.drawGrowCut = function () {
2055
2371
  console.log("bitmap dims are wrong");
2056
2372
  return;
2057
2373
  }
2058
- this.drawUndoBitmap = this.drawBitmap.slice();
2374
+ //this.drawUndoBitmap = this.drawBitmap.slice();
2059
2375
  let gl = this.gl;
2060
2376
  let fb = gl.createFramebuffer();
2061
2377
  gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
@@ -2068,15 +2384,17 @@ Niivue.prototype.drawGrowCut = function () {
2068
2384
  for (let i = 1; i < nv; i++) img16[i] = this.drawBitmap[i];
2069
2385
  let label0 = this.r16Tex(null, gl.TEXTURE6, this.back.dims, img16);
2070
2386
  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;
2387
+ let kMAX_STRENGTH = 10000;
2388
+ for (let i = 1; i < nv; i++) if (img16[i] > 0) img16[i] = kMAX_STRENGTH;
2072
2389
  let strength0 = this.r16Tex(null, gl.TEXTURE4, this.back.dims, img16);
2073
2390
  let strength1 = this.r16Tex(null, gl.TEXTURE5, this.back.dims, img16);
2074
2391
  this.gl.bindVertexArray(this.genericVAO);
2075
2392
  let shader = this.growCutShader;
2076
2393
  shader.use(gl);
2394
+ let iterations = 128; //will run 2x this value
2395
+ gl.uniform1i(shader.uniforms["finalPass"], 0);
2077
2396
  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++) {
2397
+ for (let j = 0; j < iterations; j++) {
2080
2398
  gl.uniform1i(shader.uniforms["inputTexture1"], 6); // label0 is TEXTURE6
2081
2399
  gl.uniform1i(shader.uniforms["inputTexture2"], 4); // strength0 is TEXTURE4
2082
2400
  for (let i = 0; i < this.back.dims[3]; i++) {
@@ -2104,6 +2422,7 @@ Niivue.prototype.drawGrowCut = function () {
2104
2422
  this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
2105
2423
  } //for i: each slice
2106
2424
  //reverse order strength1/label1 and strength0/label0 for reading and writing:
2425
+ if (j === iterations - 1) gl.uniform1i(shader.uniforms["finalPass"], 1);
2107
2426
  gl.uniform1i(shader.uniforms["inputTexture1"], 7); // label1 is TEXTURE7
2108
2427
  gl.uniform1i(shader.uniforms["inputTexture2"], 5); // strength1 is TEXTURE5
2109
2428
  for (let i = 0; i < this.back.dims[3]; i++) {
@@ -2132,17 +2451,9 @@ Niivue.prototype.drawGrowCut = function () {
2132
2451
  } //for j: each iteration
2133
2452
  //read data
2134
2453
  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
2454
+ let readAttach = gl.COLOR_ATTACHMENT1;
2455
+ let readTex = label0;
2144
2456
  gl.readBuffer(readAttach); //label
2145
-
2146
2457
  // assuming a framebuffer is bound with the texture to read attached
2147
2458
  const format = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT);
2148
2459
  const type = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE);
@@ -2173,12 +2484,7 @@ Niivue.prototype.drawGrowCut = function () {
2173
2484
  }
2174
2485
  let mx = img16[0];
2175
2486
  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
2487
  for (let i = 1; i < nv; i++) this.drawBitmap[i] = img16[i];
2181
-
2182
2488
  //clean up
2183
2489
  //restore textures
2184
2490
  gl.activeTexture(gl.TEXTURE2);
@@ -2199,6 +2505,7 @@ Niivue.prototype.drawGrowCut = function () {
2199
2505
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
2200
2506
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
2201
2507
  gl.deleteFramebuffer(fb);
2508
+ this.drawAddUndoBitmap();
2202
2509
  this.refreshDrawing(true);
2203
2510
  };
2204
2511
 
@@ -2289,6 +2596,87 @@ Niivue.prototype.drawLine = function (ptA, ptB, penValue) {
2289
2596
  }
2290
2597
  };
2291
2598
 
2599
+ Niivue.prototype.drawFloodFill = function (seedXYZ, newColor = 0) {
2600
+ //3D "paint bucket" fill:
2601
+ // set all voxels connected to seed point to newColor
2602
+ // https://en.wikipedia.org/wiki/Flood_fill
2603
+ newColor = Math.abs(newColor);
2604
+ let dims = [this.back.dims[1], this.back.dims[2], this.back.dims[3]]; //+1: dims indexed from 0!
2605
+ if (seedXYZ[0] < 0 || seedXYZ[1] < 0 || seedXYZ[2] < 0) return;
2606
+ if (seedXYZ[0] >= dims[0] || seedXYZ[1] >= dims[1] || seedXYZ[2] >= dims[2])
2607
+ return;
2608
+ let nx = dims[0];
2609
+ let nxy = nx * dims[1];
2610
+ let nxyz = nxy * dims[2];
2611
+ let img = this.drawBitmap.slice();
2612
+ if (img.length !== nxy * dims[2]) return;
2613
+ function xyz2vx(pt) {
2614
+ //provided an XYZ 3D point, provide address in 1D array
2615
+ return pt[0] + pt[1] * nx + pt[2] * nxy;
2616
+ }
2617
+ function vx2xyz(vx) {
2618
+ //provided address in 1D array, return XYZ coordinate
2619
+ let Z = Math.floor(vx / nxy); //slice
2620
+ let Y = Math.floor((vx - Z * nxy) / nx); //column
2621
+ let X = Math.floor(vx % nx);
2622
+ return [X, Y, Z];
2623
+ }
2624
+ let seedVx = xyz2vx(seedXYZ);
2625
+ let seedColor = img[seedVx];
2626
+ if (seedColor === newColor) {
2627
+ console.log("drawFloodFill selected voxel is already desired color");
2628
+ return;
2629
+ }
2630
+ //img is binary mask with selected color as 1 and all other colors 0
2631
+ for (let i = 1; i < nxyz; i++) {
2632
+ img[i] = 0;
2633
+ if (this.drawBitmap[i] === seedColor) img[i] = 1;
2634
+ }
2635
+ //1. Set Q to the empty queue or stack.
2636
+ let Q = [];
2637
+ //2. Add node to the end of Q.
2638
+ Q.push(seedVx);
2639
+ img[seedVx] = 2; //part of cluster
2640
+ //3. While Q is not empty:
2641
+ while (Q.length > 0) {
2642
+ //4. Set n equal to the first element of Q.
2643
+ let vx = Q[0];
2644
+ //5. Remove first element from Q.
2645
+ Q.shift();
2646
+ //6. Test six neighbors of n (left,right,anterior,posterior,inferior, superior
2647
+ // If any is is unfound part of cluster (value = 1) set it to found (value 2) and add to Q
2648
+ let xyz = vx2xyz(vx);
2649
+ function testNeighbor(offset) {
2650
+ let xyzN = xyz.slice();
2651
+ xyzN[0] += offset[0];
2652
+ xyzN[1] += offset[1];
2653
+ xyzN[2] += offset[2];
2654
+ if (xyzN[0] < 0 || xyzN[1] < 0 || xyzN[2] < 0) return;
2655
+ if (xyzN[0] >= dims[0] || xyzN[1] >= dims[1] || xyzN[2] >= dims[2])
2656
+ return;
2657
+ let vxT = xyz2vx(xyzN);
2658
+ if (img[vxT] !== 1) return;
2659
+ img[vxT] = 2; //part of cluster
2660
+ Q.push(vxT);
2661
+ }
2662
+ testNeighbor([0, 0, -1]); //inferior
2663
+ testNeighbor([0, 0, 1]); //superior
2664
+ testNeighbor([0, -1, 0]); //posterior
2665
+ testNeighbor([0, 1, 0]); //anterior
2666
+ testNeighbor([-1, 0, 0]); //left
2667
+ testNeighbor([1, 0, 0]); //right
2668
+ //7. Continue looping until Q is exhausted.
2669
+ }
2670
+ //8. Return
2671
+ //this.drawUndoBitmap = this.drawBitmap.slice();
2672
+ for (let i = 1; i < nxyz; i++)
2673
+ if (img[i] === 2)
2674
+ //if part of cluster
2675
+ this.drawBitmap[i] = newColor;
2676
+ this.drawAddUndoBitmap();
2677
+ this.refreshDrawing(false);
2678
+ }; // drawFloodFill()
2679
+
2292
2680
  Niivue.prototype.drawPenFilled = function () {
2293
2681
  let nPts = this.drawPenFillPts.length;
2294
2682
  if (nPts < 2) {
@@ -2415,17 +2803,20 @@ Niivue.prototype.drawPenFilled = function () {
2415
2803
  } // x column
2416
2804
  } //y row
2417
2805
  } //not axial
2806
+ //this.drawUndoBitmaps[this.currentDrawUndoBitmap]
2418
2807
  if (
2419
2808
  !this.drawFillOverwrites &&
2420
- this.drawUndoBitmap.length === this.drawBitmap.length
2809
+ this.drawUndoBitmaps[this.currentDrawUndoBitmap].length > 0
2421
2810
  ) {
2422
- let nv = this.drawUndoBitmap.length;
2811
+ let nv = this.drawBitmap.length;
2812
+ let bmp = decodeRLE(this.drawUndoBitmaps[this.currentDrawUndoBitmap], nv);
2423
2813
  for (let i = 0; i < nv; i++) {
2424
- if (this.drawUndoBitmap[i] === 0) continue;
2425
- this.drawBitmap[i] = this.drawUndoBitmap[i];
2814
+ if (bmp[i] === 0) continue;
2815
+ this.drawBitmap[i] = bmp[i];
2426
2816
  }
2427
2817
  }
2428
2818
  this.drawPenFillPts = [];
2819
+ this.drawAddUndoBitmap();
2429
2820
  this.refreshDrawing(false);
2430
2821
  }; // drawPenFilled()
2431
2822
 
@@ -3767,11 +4158,14 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
3767
4158
  }
3768
4159
  if (this.opts.drawingEnabled) {
3769
4160
  let pt = this.frac2vox(this.scene.crosshairPos);
4161
+ if (this.opts.penValue < 0 || Object.is(this.opts.penValue, -0)) {
4162
+ this.drawFloodFill(pt, Math.abs(this.opts.penValue));
4163
+ return;
4164
+ }
3770
4165
  if (isNaN(this.drawPenLocation[0])) {
3771
4166
  this.drawPenAxCorSag = axCorSag;
3772
4167
  this.drawPenFillPts = [];
3773
4168
  this.drawPt(...pt, this.opts.penValue);
3774
- this.drawUndoBitmap = this.drawBitmap.slice();
3775
4169
  } else {
3776
4170
  if (
3777
4171
  pt[0] === this.drawPenLocation[0] &&
@@ -3783,6 +4177,7 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
3783
4177
  }
3784
4178
  this.drawPenLocation = pt;
3785
4179
  if (this.opts.isFilledPen) this.drawPenFillPts.push(pt);
4180
+ //xxxxxx
3786
4181
  this.refreshDrawing(false);
3787
4182
  }
3788
4183
  this.drawScene();
@@ -4001,12 +4396,22 @@ Niivue.prototype.setInterpolation = function (isNearest) {
4001
4396
  this.drawScene();
4002
4397
  }; // setInterpolation()
4003
4398
 
4004
- Niivue.prototype.calculateMvpMatrixX = function () {
4005
- let whratio = this.gl.canvas.clientWidth / this.gl.canvas.clientHeight;
4399
+ Niivue.prototype.calculateMvpMatrixX = function (leftTopWidthHeight) {
4400
+ let gl = this.gl;
4401
+ gl.viewport(
4402
+ leftTopWidthHeight[0],
4403
+ leftTopWidthHeight[1],
4404
+ leftTopWidthHeight[2],
4405
+ leftTopWidthHeight[3]
4406
+ );
4407
+ let whratio = leftTopWidthHeight[2] / leftTopWidthHeight[3];
4006
4408
  //pivot from center of objects
4007
4409
  //let scale = this.furthestVertexFromOrigin;
4008
4410
  //let origin = [0,0,0];
4009
- let scale = this.furthestFromPivot;
4411
+ console.log(">>", this.back.dimsRAS, this.back.matRAS);
4412
+
4413
+ let scale = this.furthestFromPivot * 0.5;
4414
+
4010
4415
  let origin = [0, 0, 0];
4011
4416
  let projectionMatrix = mat.mat4.create();
4012
4417
  //scale = (0.8 * scale) / this.volScaleMultiplier; //2.0 WebGL viewport has range of 2.0 [-1,-1]...[1,1]
@@ -4059,7 +4464,7 @@ Niivue.prototype.calculateMvpMatrixX = function () {
4059
4464
  let modelViewProjectionMatrix = mat.mat4.create();
4060
4465
  mat.mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
4061
4466
  return [modelViewProjectionMatrix, modelMatrix, normalMatrix];
4062
- }; // calculateMvpMatrix
4467
+ }; // calculateMvpMatrixX
4063
4468
 
4064
4469
  Niivue.prototype.drawMesh3DX = function (
4065
4470
  isDepthTest = true,
@@ -4069,10 +4474,7 @@ Niivue.prototype.drawMesh3DX = function (
4069
4474
  if (this.meshes.length < 1) return;
4070
4475
  let gl = this.gl;
4071
4476
  let m, modelMtx, normMtx;
4072
- [m, modelMtx, normMtx] = this.calculateMvpMatrixX(
4073
- this.volumeObject3D,
4074
- leftTopWidthHeight
4075
- );
4477
+ [m, modelMtx, normMtx] = this.calculateMvpMatrixX(leftTopWidthHeight);
4076
4478
 
4077
4479
  gl.enable(gl.DEPTH_TEST);
4078
4480
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
@@ -4137,6 +4539,7 @@ Niivue.prototype.drawMesh3DX = function (
4137
4539
  gl.enable(gl.BLEND);
4138
4540
  gl.depthFunc(gl.ALWAYS);
4139
4541
  this.readyForSync = true;
4542
+ //gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
4140
4543
  }; //drawMesh3D()
4141
4544
 
4142
4545
  // not included in public docs
@@ -4145,7 +4548,16 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
4145
4548
  let gl = this.gl;
4146
4549
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
4147
4550
  gl.disable(gl.DEPTH_TEST);
4148
-
4551
+ /* issue56
4552
+ if (axCorSag === this.sliceTypeAxial) {
4553
+ gl.clearDepth(0.0);
4554
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
4555
+ gl.clear(gl.COLOR_BUFFER_BIT);
4556
+
4557
+ this.drawMesh3DX(true,1.0,leftTopWidthHeight);
4558
+ this.setPivot3D();
4559
+ return;
4560
+ }*/
4149
4561
  var crossXYZ = [
4150
4562
  this.scene.crosshairPos[0],
4151
4563
  this.scene.crosshairPos[1],
@@ -4267,9 +4679,8 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
4267
4679
  ],
4268
4680
  "S"
4269
4681
  );
4682
+
4270
4683
  if (axCorSag !== this.sliceTypeAxial) return;
4271
- // gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
4272
- this.setPivot3D();
4273
4684
  gl.viewport(
4274
4685
  leftTopWidthHeight[0],
4275
4686
  leftTopWidthHeight[1],
@@ -4279,6 +4690,7 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
4279
4690
  gl.enable(gl.DEPTH_TEST);
4280
4691
  gl.depthFunc(gl.ALWAYS);
4281
4692
  gl.clearDepth(0.0);
4693
+
4282
4694
  this.sync();
4283
4695
  }; // draw2D()
4284
4696